utils::globalVariables(c(
  "..colsKeep", "..colsToNAfill", "..removeCols", ".I", ".N",
  "Archs", "AvailableVersion", "compareVersionAvail", "correctVersion",
  "dayAfterPutOnCRAN", "DepVersion", "destFile", "dup", "filepath",
  "fullGit", "github", "groupCRANtogether", "groupCRANtogetherChange",
  "groupCRANtogetherDif", "hasVersionSpec", "i.neededFiles",
  "inequality", "installFrom", "installFromFac", "installOrder",
  "installResult", "isGitPkg", "keep", "keep2", "lastRow", "loadedSufficient",
  "localFileName", "localType", "maxVers", "mtime", "N", "Names", "neededFiles",
  "needLaterDate", "nextRow", "Package", "packageFullName", "repoLocation",
  "RepoWBranch", "tmpOrder", "type", "version", "VersionFromPV", "violations"
))

#' Parse a github package specification
#'
#' This converts a specification like `PredictiveEcology/Require@development`
#' into separate columns, "Account", "Repo", "Branch", "GitSubFolder" (if there is one)
#'
#' @details
#' `parseGitHub` turns the single character string representation into 3 or 4:
#' `Account`, `Repo`, `Branch`, `SubFolder`.
#'
#' @return
#' `parseGitHub` returns a `data.table` with added columns.
#'
#' @family version specifications
#' @export
#' @rdname GitHubTools
#' @param pkgDT A pkgDT data.table.
#' @inheritParams Require
parseGitHub <- function(pkgDT, verbose = getOption("Require.verbose")) {
  pkgDT <- toPkgDT(pkgDT)

  if (is.null(pkgDT$githubPkgName)) {
    set(pkgDT, NULL, "githubPkgName", extractPkgGitHub(pkgDT$packageFullName))
    isGH <- !is.na(pkgDT$githubPkgName)
    if (is.null(pkgDT$repoLocation)) {
      set(pkgDT, which(isGH), "repoLocation", .txtGitHub)
      set(pkgDT, which(!isGH), "repoLocation", "CRAN")
    }

    if (any(pkgDT$repoLocation == .txtGitHub)) {
      isGH <- pkgDT$repoLocation == .txtGitHub
      isGitHub <- which(isGH)
      set(pkgDT, isGitHub, "fullGit", trimVersionNumber(pkgDT$packageFullName[isGitHub]))
      set(pkgDT, isGitHub, "fullGit", masterMainToHead(pkgDT$fullGit[isGitHub]))
      set(pkgDT, isGitHub, "Account", gsub("^(.*)/.*$", "\\1", pkgDT$fullGit[isGitHub]))
      set(pkgDT, isGitHub, "RepoWBranch", gsub("^(.*)/(.*)@*.*$", "\\2", pkgDT$fullGit[isGitHub]))
      set(pkgDT, isGitHub, "hasSubFolder", grepl("/", pkgDT$Account[isGitHub]))
      if (any(pkgDT$hasSubFolder, na.rm = TRUE)) { # fix both Account and RepoWBranch
        hasSubFold <- which(pkgDT$hasSubFolder)
        subFoldIndices <- seq_len(NROW(pkgDT[hasSubFold]))
        set(pkgDT, hasSubFold, "Account", gsub("^(.*)/(.*)$", "\\1", pkgDT$Account[hasSubFold]))
        pkgDT[hasSubFolder %in% TRUE,
              RepoWBranch := gsub(paste0("^", Account, "/"), "", fullGit),
              by = seq(sum(hasSubFolder, na.rm = TRUE))
        ]
        pkgDT[hasSubFolder %in% TRUE,
              GitSubFolder := strsplit(RepoWBranch, split = "/|@")[[1]][2],
              by = seq(sum(hasSubFolder, na.rm = TRUE))
        ]
        pkgDT[hasSubFolder %in% TRUE,
              RepoWBranch := gsub(paste0("/", GitSubFolder), "", RepoWBranch),
              by = seq(sum(hasSubFolder, na.rm = TRUE))
        ]
      }
      set(pkgDT, isGitHub, "Repo", gsub("^(.*)@(.*)$", "\\1", pkgDT$RepoWBranch[isGitHub]))
      # set(pkgDT, isGitHub, "Branch", "HEAD")
      set(pkgDT, isGitHub, "Branch", "main")
      wh1 <- which(isGH & grepl("@", pkgDT$RepoWBranch))
      set(pkgDT, wh1, "Branch", gsub("^.*@(.*)$", "\\1", pkgDT$RepoWBranch[wh1]))
      set(pkgDT, NULL, c("RepoWBranch", "fullGit"), NULL)
    }
  }
  pkgDT[]
}

#' @return `DESCRIPTIONFileVersionV()` and `DESCRIPTIONFileOtherV()` return a character vector with one element per `file` (the `Version` field, or the field named in `other`); `dlGitHubDESCRIPTION()` returns the `data.table` of parsed GitHub refs with the local path of each downloaded DESCRIPTION file in a `DESCFile` column.
#' @rdname DESCRIPTION-helpers
#' @param file A file path to a DESCRIPTION file
DESCRIPTIONFileVersionV <- function(file, purge = getOption("Require.purge", FALSE)) {
  if (is.null(envPkgDepDESCFile())) purge <- dealWithCache(purge, checkAge = FALSE)
  out <- lapply(file, function(f) {
    out <- if (!is.null(envPkgDepDESCFile())) {
      if (purge && length(f) == 1) suppressWarnings(rm(f, envir = envPkgDepDESCFile()))
      if (length(f) == 1) {
        get0(f, envir = envPkgDepDESCFile())
      } else {
        f
      }
    } else {
      NULL
    }

    if (isTRUE(any(file.exists(f))))
      lines <- readLinesWithHandlers(f)
    else
      lines <- f
    # if (length(f) == 1) {
    #   withCallingHandlers(
    #     lines <- try(readLines(f), silent = TRUE),
    #     warning = function(w)
    #       if (grepl('incomplete final line found on', w$message))
    #         invokeRestart("muffleWarning")
    #   )
    #   if (is(lines, "try-error")) {
    #     warning(lines)
    #     lines <- character()
    #   }
    #   if (isTRUE(any(grepl("404: Not Found", lines))))
    #     lines <- character()
    # } else {
    #   lines <- f
    # }
    suppressWarnings({
      vers_line <- lines[grep("^Version: *", lines)]
    })
    out <- gsub("Version: ", "", vers_line)
    if (length(out) == 0) out <- NA
    if (length(f) == 1) {
      assign(f, out, envir = envPkgDepDESCFile())
    }
    out
  })
  unlist(out)
}

#' @rdname DESCRIPTION-helpers
#' @param file A file path to a `DESCRIPTION` file
#' @param other Any other keyword in a `DESCRIPTION` file that precedes a ":".
#'   The rest of the line will be retrieved.
DESCRIPTIONFileOtherV <- function(file, other = "RemoteSha") {
  out <- lapply(file, function(fff) {
    lines <- readLinesWithHandlers(fff)
    suppressWarnings({
      vers_line <- lines[grep(paste0("^", other, ": *"), lines)]
    })
    out <- gsub(paste0(other, ": "), "", vers_line)
    if (length(out) == 0) out <- NA
    if (length(out) > 1) out <- tail(out, 1)
    out
  })
  unlist(out)
}

.compareVersionV <- Vectorize(compareVersion)
.evalV <- Vectorize(eval, vectorize.args = "expr")
.parseV <- Vectorize(parse, vectorize.args = "text")

#' GitHub package tools
#'
#' A series of helpers to access and deal with GitHub packages
#'
#' @details
#' `dlGitHubDESCRIPTION` retrieves the DESCRIPTION file from GitHub.com
#'
#' @rdname DESCRIPTION-helpers
#' @export
#' @param pkg A character string with a GitHub package specification (c.f. remotes)
#' @inheritParams pkgDep
#' @inheritParams Require
dlGitHubDESCRIPTION <- function(pkg, purge = getOption("Require.purge", FALSE),
                                verbose = getOption("Require.verbose")) {
  dlGitHubFile(pkg, "DESCRIPTION", purge = purge, verbose = verbose)
}

#' @inheritParams Require
dlGitHubNamespace <- function(pkg, purge = getOption("Require.purge", FALSE),
                              verbose = getOption("Require.verbose")) {
  dlGitHubFile(pkg, "NAMESPACE", purge = purge, verbose = verbose)
}

pkgDTtoPackageFullName <- function(pkg) {
  if (is.data.table(pkg)) {
    if (!all(c("Account", "Repo", "Branch") %in% colnames(pkg))) {
      if (any(c("packageFullName") %in% colnames(pkg))) {
        pkg <- pkg$packageFullName
      }
    }
  }
  pkg
}

#' @inheritParams Require
dlGitHubFile <- function(pkg, filename = "DESCRIPTION",
                         purge = getOption("Require.purge", FALSE),
                         verbose = getOption("Require.verbose")) {
  ret <- if (NROW(pkg) > 0) {
    needsParse <- TRUE
    cn <- colnames(pkg)
    if (!is.null(cn)) {
      needsParse <- !all(c("hasSubFolder", "Repo", "Branch", "Account") %in% cn)
    }
    if (needsParse) {
      pkg <- pkgDTtoPackageFullName(pkg)
      pkgDT <- parseGitHub(pkg, verbose = verbose)
    } else {
      pkgDT <- pkg
    }
    if (!is.null(pkgDT[["shas"]])) {
      pkgDT[nchar(pkgDT[["shas"]]) > 0, Branch := shas]
    }
    if (is.null(pkgDT[["hasSubFolder"]])) set(pkgDT, NULL, "hasSubFolder", FALSE)
    pkgDT[repoLocation == .txtGitHub,
          url := {
            gitHubFileUrl(
              hasSubFolder = hasSubFolder, Branch = Branch, GitSubFolder = GitSubFolder,
              Account = Account, Repo = Repo, filename = filename
            )
          },
          by = "Package"
    ]
    destFile <- RequireGitHubCacheFile(pkgDT, filename = filename)

    feDF <- file.exists(destFile)
    if (isTRUE(any(feDF))) {
      destFile2 <- destFile[feDF]
      versionLocal <- DESCRIPTIONFileVersionV(destFile2)
      versionLocalOK <- rep(TRUE, length(versionLocal)) # no versionSpec will give NA next; NA is "keep"
      anyHEAD <- (pkgDT$versionSpec[pkgDT$repoLocation == .txtGitHub][feDF] == "HEAD")
      hasNonHead <- anyHEAD %in% FALSE
      if (isTRUE(any(anyHEAD %in% TRUE))) {
        # check if it is in this call to Require or pkgDep, based on time: delete if not
        stRequire <- get0("stRequire", envir = whereInStack("stRequire"))
        if (!is.null(stRequire)) {
          dtime <- difftime(stRequire, file.info(destFile2)[, "mtime"], units = "secs")
          whThisCall <- dtime < 0
          if (any(whThisCall))
            anyHEAD[whThisCall] <- FALSE
        }
        versionLocalOK[anyHEAD] <- FALSE
      }
      if (isTRUE(any(hasNonHead)))
        versionLocalOK <- compareVersion2(versionLocal[hasNonHead],
                                          pkgDT$versionSpec[pkgDT$repoLocation == .txtGitHub][feDF][hasNonHead],
                                          inequality = pkgDT$inequality[feDF][hasNonHead])
      versionLocalNotOK <- versionLocalOK %in% FALSE
      if (isTRUE(any(versionLocalNotOK)) && getOption("Require.offlineMode") %in% FALSE) {
        oo <- file.remove(unique(destFile2[versionLocalNotOK]))
      }
    } else {
      # NOT CLEAR WHAT SHOULD BE PUT HERE
      # destFile <- NA
    }
    set(pkgDT, NULL, "destFile", destFile)

    if (!isTRUE(getOption("Require.offlineMode"))) {
      alreadyExists <- rmEmptyFiles(pkgDT$destFile)
      if (any(alreadyExists)) {
        fs <- file.size(pkgDT$destFile)
        tooSmall <- fs < 100
        if (any(tooSmall %in% TRUE)) {
          unlink(pkgDT$destFile[which(tooSmall)])
          alreadyExists <- tooSmall %in% FALSE
        }
      }
      if (any(!alreadyExists)) {
        # withCallingHandlers( # if offline
        pkgDT[which(repoLocation == .txtGitHub & alreadyExists %in% FALSE),
              filepath :=
                .downloadFileMasterMainAuthWithHandlers(Account, Package, Branch,
                                                        packageFullName, filename,
                                                        url, destFile, verbose)
              ,
              by = c("Package", "Branch")
        ]# , warning = function(w) {
        #   ## TODO this seems to be not relevant
        # })
      }
      old <- grep("filepath|destFile", colnames(pkgDT), value = TRUE)[1]
      wh <- which(pkgDT$repoLocation == .txtGitHub)
      DESCFileVals <- pkgDT[[old]][wh]
      if (identical("DESCRIPTION", filename)) {
        cn <- "DESCFile"
        # set(pkgDT, wh, "DESCFile", pkgDT[[old]][wh])
      } else {
        cn <- "filename"
        # set(pkgDT, wh, "filename", pkgDT[[old]][wh])
      }

    } else {
      wh <- NULL
      DESCFileVals <- pkgDT[["destFile"]]
      cn <- "DESCFile"
    }
    set(pkgDT, wh, "DESCFile", DESCFileVals)

    pkgDT[]
  } else {
    pkg
  }
  ret
}

#' Available and archived versions
#'
#' These are wrappers around available.packages and also get the archived versions
#' available on CRAN.
#'
#' @return `dlArchiveVersionsAvailable()`: a list with one `data.table` per `package`, the
#'   versions found in the CRAN archive (`repo`, `PackageUrl` and file metadata), empty when
#'   none. `available.packagesCached()`: the available-packages table for `repos`, as a
#'   `data.table` (default) or a matrix as `utils::available.packages()` returns it.
#' @rdname availableVersions
#' @export
#' @param package A single package name (without version or github specifications)
#' @details
#' `dlArchiveVersionsAvailable` searches CRAN Archives for available versions.
#' It has been borrowed from a sub-set of the code in a non-exported function:
#' `remotes:::download_version_url`
dlArchiveVersionsAvailable <- function(package, repos = getOption("repos"), verbose = getOption("Require.verbose")) {
  info <- list()
  for (repo in repos) {
    archiveFile <- archiveFile(repo) # sprintf("%s/src/contrib/Meta/archive.rds", repo)
    if (!exists(archiveFile, envir = pkgDepEnv(), inherits = FALSE)) {
      archive <- tryCatch(
        {
          con <- gzcon(url(archiveFile, "rb"))
          on.exit(close(con))
          readRDS(con)
        },
        warning = function(e) {
          # "cannot open URL 'https://predictiveecology.r-universe.dev/src/contrib/Meta/archive.rds': HTTP status was '404 Not Found'"
          #  this seems to be because r-universe.dev doesn't keep archives
          options(Require.checkInternet = TRUE)
          if (!internetExists())
            setOfflineModeTRUE(verbose = verbose)
          #
          #
          list()
        },
        error = function(e) {
          list()
        }
        )
if (length(archive))
  assign(archiveFile, archive, envir = pkgDepEnv())
    } else {
      archive <- get(archiveFile, envir = pkgDepEnv())
    }
    if (length(archive) == 0) {
      archive <- Map(pack = package, function(pack) NULL)
    }
    info[[repo]] <- archive[package]
    naNames <- is.na(names(info[[repo]]))
    if (any(naNames)) {
      names(info[[repo]])[naNames] <- package[naNames]
    }
    if (!is.null(info[[repo]][[1]])) {
      info[[repo]] <- lapply(info[[repo]], function(x) {
        x$repo <- repo
        x
      })
    }
  }
  info <- invertList(info)
  # info <- lapply(info, unname)
  info <- lapply(info, function(dd) lapply(dd, function(d) as.data.table(d, keep.rownames = "PackageUrl")))
  info <- lapply(info, rbindlist, idcol = "repo", fill = TRUE, use.names = TRUE)
  # info <- lapply(info, rbindlist)
  info <- lapply(info, function(d) {
    if (!is.null(d[["mtime"]])) setorderv(d, "mtime")
  })



  return(info)
}


#' @importFrom utils packageVersion installed.packages
installedVers <- function(pkgDT, libPaths, standAlone = FALSE) {

  pkgDT <- toPkgDT(pkgDT)
  # pp <- data.table::copy(pkgDT)
  if (NROW(pkgDT)) {
    # ip2 <- as.data.table(installed.packages(lib.loc = libPaths, fields = c("Package", "LibPath", "Version")))
    if (isTRUE(standAlone)) {
      lp <- libPaths[1]
    } else {
      lp <- libPaths
    }
    ip <- as.data.table(.installed.pkgs(lib.loc = lp, other = "LibPath", which = NULL, packages = pkgDT$Package)) # , other = c("Package", "Version"))) # these 2 are defaults
    ip <- ip[ip$Package %in% pkgDT$Package]
    if (NROW(ip)) {
      pkgs <- pkgDT$Package
      names(pkgs) <- pkgDT$packageFullName
      ln <- loadedNamespaces()
      ln <- ln[!ln %in% .basePkgs]
      # Need both the next lines
      pkgs <- pkgs[pkgs %in% ln]
      pkgs <- pkgs[pkgs %in% ip$Package] # can be loadedNamespace, but not installed, if it had been removed in this session
      if (NROW(pkgs)) {
        pkgs <- pkgs[!duplicated(pkgs)]

        installedPkgsCurrent <- data.table(Package = pkgs, packageFullName = names(pkgs))
        installedPkgsCurrent[, VersionFromPV := tryCatch({
          lp <- ip$LibPath[ip$Package %in% Package][1]
          as.character(packageVersion(Package, lp))
        }, error = function(e) NA_character_), by = "Package"]
        ip <- try(installedPkgsCurrent[ip, on = "Package"])
        if (is(ip, "try-error")) {
          browserDeveloper("Error 234")
        }
        ip[!is.na(VersionFromPV), Version := VersionFromPV]
      }
    }
    ip <- ip[, c("Package", "LibPath", "Version")]
    ip <- unique(ip, by = c("Package")) # , "LibPath" # basically, only take the first one if 2 installed in LibPath
    pkgDT <- try(ip[pkgDT, on = "Package"], silent = TRUE)
    if (is(pkgDT, "try-error")) {
      browserDeveloper("Error 123")
    }

  } else {
    pkgDT <- cbind(pkgDT, LibPath = NA_character_, "Version" = NA_character_)
  }

  installed <- !is.na(pkgDT$Version)
  set(pkgDT, NULL, "installed", installed)
  pkgDT
}

# When a package is already loaded in the current R session with a version
# that satisfies the user's version constraint, mark it as installed so the
# downstream gate skips reinstall. Trying to upgrade a loaded package whose
# namespace is imported by another loaded package (e.g. `reproducible` ->
# `climateData`) is the most common cause of pak's
# "Error : ! error in pak subprocess" — pak can't unload the live namespace
# to swap in the new version, the subprocess aborts, and the user is left
# with a useless generic error. If the loaded version already meets the
# requested constraint, there is no reason to reinstall in the first place.
#
# Side-effect: also flags `loadedSufficient = TRUE` so doLoads() can attach
# via `require(x, character.only = TRUE)` without `lib.loc`, avoiding R's
# "cannot be unloaded because <X> is imported by <Y>" error path.
useLoadedIfSufficient <- function(pkgDT,
                                  libPaths = .libPaths(),
                                  standAlone = FALSE,
                                  verbose = getOption("Require.verbose")) {
  if (!NROW(pkgDT)) return(pkgDT)
  if (!"needInstall" %in% names(pkgDT)) return(pkgDT)
  candidates <- which(pkgDT[["needInstall"]] %in% .txtInstall)
  if (!length(candidates)) return(pkgDT)
  loaded <- loadedNamespaces()
  loaded <- setdiff(loaded, .basePkgs)
  if (!length(loaded)) return(pkgDT)
  if (!"loadedSufficient" %in% names(pkgDT))
    set(pkgDT, NULL, "loadedSufficient", FALSE)
  # `(HEAD)` is a Require-specific pin that means "the latest commit of the
  # branch in the ref" (e.g. `account/repo@somebranch (HEAD)` -> always fetch
  # the tip of `somebranch`). It is not encoded in versionSpec/inequality, so
  # the constraint check below would treat it as unconstrained and skip
  # reinstall on whatever stale version happens to be loaded. Populate the
  # `hasHEAD` column up front so we can keep those refs out of the
  # already-loaded fast path.
  pkgDT <- checkHEAD(pkgDT)
  # standAlone = TRUE means the user wants the package physically present in
  # libPaths[1]; a namespace loaded from another library does NOT satisfy that.
  effectiveLibPaths <- if (isTRUE(standAlone))
    normPath(libPaths[1L]) else normPath(libPaths)
  intercepted <- character(0)
  reasons <- character(0)
  for (i in candidates) {
    pkg <- pkgDT[["Package"]][i]
    if (!pkg %in% loaded) next
    # Skip refs pinned to HEAD: a loaded namespace can never satisfy "must be
    # the current tip of branch X" because we have no commit hash to compare.
    # Forward to the normal install path, which lets pak fetch the tip.
    if (isTRUE(pkgDT[[hasHEADtxt]][i])) next
    loadedVer <- tryCatch(as.character(getNamespaceVersion(pkg)),
                          error = function(e) NA_character_)
    if (is.na(loadedVer) || !nzchar(loadedVer)) next
    vSpec <- pkgDT[["versionSpec"]][i]
    ineq  <- pkgDT[["inequality"]][i]
    hasConstraint <- !is.na(vSpec) && nzchar(vSpec) &&
                     !is.na(ineq) && nzchar(ineq)
    if (hasConstraint) {
      ok <- isTRUE(compareVersion2(loadedVer, vSpec, ineq))
      if (!ok) next
    }
    # suppressWarnings: this is a best-effort "where does pkg live" probe. When a
    # namespace stays loaded after its files were removed (the very case the
    # hasDescOnDisk check below handles), system.file()/find.package() walk the
    # libPaths and read.dcf() the stale DESCRIPTION path, emitting "cannot open
    # compressed file '<lib>/<pkg>/DESCRIPTION'". (Pronounced under devtools::test,
    # where pkgload shims system.file().) tryCatch only traps errors, so that
    # warning leaked out and tripped tests asserting a clean warning set. The
    # missing-files case is already handled below; the warning is pure noise here.
    lp <- suppressWarnings(tryCatch(dirname(system.file(package = pkg)),
                                    error = function(e) NA_character_))
    if (!nzchar(lp)) lp <- NA_character_
    if (!is.na(lp) && !normPath(lp) %in% effectiveLibPaths) next
    ## Disk-presence check: a namespace can stay loaded after its files are
    ## removed (e.g. `remove.packages("dplyr")` in the same R session).
    ## `system.file(package = pkg)` then returns the path that was recorded
    ## when the namespace loaded -- pointing at a directory that no longer
    ## exists. Without this check, useLoadedIfSufficient would skip reinstall,
    ## leaving the on-disk libPath empty and triggering downstream
    ## `read.dcf(... DESCRIPTION)` warnings the first time anything walks
    ## installed.packages(). Require's contract here is "after this call, the
    ## packages are on disk", so re-route to install when files are gone.
    hasDescOnDisk <- any(vapply(effectiveLibPaths, function(L) {
      file.exists(file.path(L, pkg, "DESCRIPTION"))
    }, logical(1)))
    if (!hasDescOnDisk) next
    set(pkgDT, i, "installed",          TRUE)
    set(pkgDT, i, "installedVersionOK", TRUE)
    set(pkgDT, i, "needInstall",        .txtDontInstall)
    set(pkgDT, i, "Version",            loadedVer)
    if (!is.na(lp)) set(pkgDT, i, "LibPath", lp)
    set(pkgDT, i, "loadedSufficient",   TRUE)
    intercepted <- c(intercepted, pkg)
    reasons <- c(reasons,
                 if (hasConstraint)
                   paste0(pkg, " ", loadedVer, " satisfies ", ineq, " ", vSpec)
                 else
                   paste0(pkg, " ", loadedVer, " (no version constraint)"))
  }
  if (length(intercepted)) {
    messageVerbose(
      "Already loaded with sufficient version, skipping reinstall: ",
      paste(unique(reasons), collapse = "; "),
      verbose = verbose, verboseLevel = 1)
  }
  pkgDT
}

#' @importFrom utils available.packages
#' @rdname availableVersions
#' @param returnDataTable Logical. If `TRUE`, the default, then the return
#'   is a data.table.
#'   Otherwise, it is a `matrix`, as per `available.packages`
#' @inheritParams Require
#' @inheritParams utils::install.packages
available.packagesCached <- function(repos, purge, verbose = getOption("Require.verbose"),
                                     returnDataTable = TRUE, type) {
  fillDefaults(pkgDep)
  if (!isTRUE(getOption("Require.offlineMode"))) {
    repos <- getCRANrepos(repos, ind = 1)
    purge <- purgeAvailablePackages(repos, purge = purge)
    # purge <- dealWithCache(purge = purge)
  } else {
    purge <- FALSE
  }
  cap <- list()
  isMac <- isMacOS()#tolower(SysInfo["sysname"]) == "darwin"
  isOldMac <- isMac && compareVersion(as.character(getRversion()), "4.0.0") < 0
  isWindows <- isWindows()

  if (identical(type, "both")) {
    types <- if (isOldMac) {
      c("mac.binary.el-capitan", "source")
    } else if (!isWindows && !isMac) {
      c("source")
    } else {
      c("binary", "source")
    }
  } else {
    types <- type
  }

  missingHttp <- !startsWith(unlist(repos), "http")
  if (any(missingHttp)) {
    repos[missingHttp] <- lapply(repos[missingHttp], function(r) {
      paste0("https://", r)
    })
  }
  if (is.list(repos)) {
    nams <- names(repos)
    repos <- unlist(repos)
    names(repos) <- nams
  }

  reposNoHttp <- gsub("^https*:*/*/*", "", repos)
  reposShort <- paste(substr(unlist(lapply(strsplit(reposNoHttp, "//"), function(x) {
    tryCatch(x[[1]], silent = TRUE,
             error = function(e) tryCatch(getOption("repos"), silent = TRUE,
                                          error = function(f) "unknown"))
    })), 1, 20), collapse = "_")
  typesShort <- paste(unlist(lapply(strsplit(types, "//"), function(x) x[[1]])), collapse = "_")
  objNam <- paste0("availablePackages", "_", reposShort, "_", typesShort)
  # existsObjNam <- exists(objNam, envir = pkgDepEnv())
  out <- get0(objNam, envir = pkgDepEnv(), inherits = FALSE)

  if (is.null(out) || NROW(out) == 0 || isTRUE(purge)) {
    for (type in types) {
      fn <- availablePackagesCachedPath(repos, type)
      purgeTime <- purgeBasedOnTimeSinceCached(file.info(fn)[, "mtime"])
      purge <- purge || purgeTime
      if (isTRUE(purge)) {
        unlink(fn)
      }
      rmEmptyFiles(fn, 200)
      needNewFile <- TRUE
      if (file.exists(fn)) {
        # can be interupted and be corrupted
        cap[[type]] <- try(readRDS(fn), silent = TRUE)
        if (!is(cap[[type]], "try-error")) needNewFile <- FALSE
        # This is case where the previous version is NROW 0; could have happened if internet was down or other
        if (NROW(cap[[type]]) == 0) needNewFile <- TRUE
      }
      if (isTRUE(needNewFile)) {
        caps <- lapply(repos, function(repo) {
          available.packagesWithCallingHandlers(repo, type, verbose = verbose)
        })
        # cachePurge may have been used to reset the available.packages cache
        val <- Sys.getenv("R_AVAILABLE_PACKAGES_CACHE_CONTROL_MAX_AGE")
        if (nzchar(val))
          if (isTRUE(val == 0))
            Sys.unsetenv("R_AVAILABLE_PACKAGES_CACHE_CONTROL_MAX_AGE")

        caps <- lapply(caps, as.data.table)
        caps <- unique(rbindlist(caps, fill = TRUE, use.names = TRUE), by = c("Package", "Version", "Repository"))
        cap[[type]] <- caps

        if (nzchar(.requirePkgInfoDir()) && NROW(caps) > 0) {
          checkPath(dirname(fn), create = TRUE)
          saveRDS(cap[[type]], file = fn)
        }
      }
    }
    cap <- rbindlist(cap, fill = TRUE, use.names = TRUE)
    assign(objNam, cap, envir = pkgDepEnv())
    out <- cap
  } else {

  }

  if (isFALSE(returnDataTable)) {
    # as.matrix is not rich enough ... do it manually
    bb <- as.matrix(out)
    rownames(bb) <- out[["Package"]]
    dimnames(bb)[[1]] <- unname(bb[, "Package"])
    out <- bb
  }



  return(out)
}

isBinary <- function(fn, needRepoCheck = TRUE, repos = getOption("repos")) {
  theTest <- (endsWith(fn, "zip") & isWindows()) |
    (grepl("R_x86", fn) & !isWindows() & !isMacOS()) |
    (endsWith(fn, macBinaryFileExtNoDot) & isMacOS())
  if (isTRUE(needRepoCheck)) {
    if (isWindows() || isMacOS()) {
      binRepo <- isBinaryCRANRepo(curCRANRepo = repos)
    } else {
      binRepo <- isBinaryCRANRepo()
    }
    theTest <- theTest | binRepo
  }
  theTest
}

isBinaryCRANRepo <- function(curCRANRepo = NULL,
                             repoToTest = formals(setLinuxBinaryRepo)[["binaryLinux"]]) {
  if (is.null(curCRANRepo)) {
    r <- getOption("repos")
    # `r[["CRAN"]]` throws "subscript out of bounds" on a named character
    # vector when no element is named "CRAN" -- can happen if a caller has
    # rebuilt `options("repos")` in a way that drops names (e.g. via
    # `unique(c(extraRepo, getOption("repos")))`).
    curCRANRepo <- if ("CRAN" %in% names(r)) r[["CRAN"]] else NA_character_
  }
  if (isWindows() || isMacOS()) {
    isBin <- grepl("[\\|/])|/bin[\\|/]", curCRANRepo)
  } else {
    if (is.name(repoToTest))
      repoToTest <- eval(repoToTest)
    isBin <- tryCatch(startsWith(prefix = repoToTest, curCRANRepo),
                     silent = TRUE, error = function(x) FALSE)
  }
  isBin
}

toPkgDT <- function(pkgDT, deepCopy = FALSE) {
  if (!is.data.table(pkgDT)) {
    pkgDT <- rmExtraSpaces(pkgDT)
    pkgDT <- if (deepCopy) {
      data.table(Package = extractPkgName(pkgDT), packageFullName = pkgDT)
    } else {
      toDT(Package = extractPkgName(pkgDT), packageFullName = pkgDT)
    }
  }

  pkgDT
}

toDT <- function(...) {
  setDT(list(...))
}


#' Detach and unload all packages
#'
#' This uses `pkgDepTopoSort` internally so that the package
#' dependency tree is determined, and then packages are unloaded
#' in the reverse order. Some packages don't unload successfully for
#' a variety of reasons. Several known packages that have this problem
#' are identified internally and *not* unloaded. Currently, these are
#' `glue`, `rlang`, `ps`, `ellipsis`, and, `processx`.
#'
#' @return
#' A numeric named vector, with names of the packages that were attempted.
#' `2` means the package was successfully unloaded, `1` it was
#' tried, but failed, `3` it was not loaded, so was not unloaded.
#' @export
#' @param pkgs A character vector of packages to detach. Will be topologically sorted
#'   unless `doSort` is `FALSE`.
#' @param dontTry A character vector of packages to not try. This can be used
#'   by a user if they find a package fails in attempts to unload it, e.g., "ps"
#' @param doSort If `TRUE` (the default), then the `pkgs` will be
#'   topologically sorted. If `FALSE`, then it won't. Useful if the
#'   `pkgs` are already sorted.
#' @inheritParams Require
#' @importFrom utils sessionInfo
#'
#'
detachAll <- function(pkgs, dontTry = NULL, doSort = TRUE, verbose = getOption("Require.verbose")) {
  messageVerbose("Detaching is fraught with many potential problems; you may have to ",
                 "restart your session if things aren't working",
                 verbose = verbose, verboseLevel = 2
  )
  srch <- search()
  pkgsOrig <- pkgs
  origDeps <- pkgDep(pkgs, recursive = TRUE)
  depsToUnload <- c(pkgs, unname(unlist(origDeps)))
  allLoaded <- loadedNamespaces()
  # si <- sessionInfo() # sessionInfo can't handle Require when loaded with load_all, under some conditions
  # allLoaded <- c(names(si$otherPkgs), names(si$loadedOnly))
  others <- pkgDepTopoSortMemoise(pkgs = pkgs, deps = allLoaded, reverse = TRUE, verbose = verbose, purge = FALSE)
  names(others) <- others
  depsToUnload <- c(others, depsToUnload)
  depsToUnload <- depsToUnload[!duplicated(depsToUnload)]
  depsToUnload <- setdiff(depsToUnload, dontTry)

  if (length(depsToUnload) > 0) {
    out <- if (isTRUE(doSort)) pkgDepTopoSortMemoise(pkgs = depsToUnload, purge = FALSE) else NULL
    pkgs <- rev(c(names(out), pkgs))
  }
  pkgs <- extractPkgName(pkgs)
  pkgs <- unique(pkgs)
  names(pkgs) <- pkgs

  dontTryExtra <- intersect(
    c("glue", "rlang", "ps", "ellipsis", "processx", "vctrs", "RCurl", "bitops"),
    pkgs
  )

  if (length(dontTryExtra)) {
    messageVerbose("some packages don't seem to unload their dlls correctly. ",
                   "These will not be unloaded: ", paste(dontTryExtra, collapse = comma),
                   verbose = verbose, verboseLevel = 2
    )
    dontTry <- c(dontTry, dontTryExtra)
  }

  dontTry <- unique(c(.RequireDependenciesNoBase, "covr", dontTry))
  didntDetach <- intersect(dontTry, pkgs)
  pkgs <- setdiff(pkgs, dontTry)
  dontNeedToUnload <- logical()
  detached <- c()
  if (length(pkgs)) {
    pkgs <- unique(pkgs)
    names(pkgs) <- pkgs
    isLoaded <- unlist(lapply(pkgs, isNamespaceLoaded))
    dontNeedToUnload <- rep(NA, sum(!isLoaded))
    names(dontNeedToUnload) <- pkgs[!isLoaded]
    pkgs <- pkgs[isLoaded]
    detached1 <- logical()
    for (pkg in pkgs) {
      det <- try(unloadNamespace(pkg), silent = TRUE)
      detached1[[pkg]] <- if (!is(det, "try-error")) TRUE else FALSE
    }
    detached <- detached1

    # detached1 <- try(sapply(pkgs, unloadNamespace))
    # if (!is(detached1, "try-error")) {
    #   detached1 <- try(sapply(detached1, is.null))
    #   if (!is.list(detached1))
    #     detached <- detached1
    # }
  }
  if (length(didntDetach)) {
    notDetached <- rep(FALSE, length(didntDetach))
    names(notDetached) <- didntDetach
    detached <- c(detached, notDetached)
  }
  detached <- c(dontNeedToUnload, detached)
  inSearchPath <- unlist(lapply(rev(pkgsOrig), function(p) {
    pkgGrp <- "package:"
    pkgString <- paste0(pkgGrp, p)
    pkgString <- grep(pkgString, srch, value = TRUE)
    pkgString <- gsub(pkgGrp, "", pkgString)
  }))
  detached[unlist(detached) %in% TRUE] <- 2
  detached[unlist(detached) %in% FALSE] <- 1
  detached[is.na(unlist(detached))] <- 3
  detached
}

isWindows <- function() {
  tolower(SysInfo["sysname"]) == "windows"
}

isMacOS <- function() {
  isMac <- tolower(SysInfo["sysname"]) == "darwin"
}

isLinux <- function() {
  isMac <- tolower(SysInfo["sysname"]) == "linux"
}

isUbuntuOrDebian <- function() {
  isTRUE(grepl("Ubuntu|Debian", utils::osVersion, ignore.case = TRUE))
}
warningCantInstall <- function(pkgs, libPaths = .libPaths()) {
  warning(
    "Can't install ", pkgs, "; you will likely need to restart R and run:\n",
    "-----\n",
    "install.packages(c('", paste(pkgs, collapse = comma), "'), lib = '", libPaths[1], "')",
    "\n-----\n...before any other packages get loaded"
  )
}


## Internal helper: given a path, append `versionMajorMinor()` so libraries
## stay R-version-specific (unless `exact = TRUE`, or the path is in
## `R_LIBS_SITE`, or we're in a non-interactive R CMD check tmp libpath).
## Default path is `cachePkgDir()`; passing `NULL` or `FALSE` short-circuits
## to `NULL`. Kept callable as `Require:::rpackageFolder` for the test
## suite and any downstream package that reaches in via the unexported
## name.
rpackageFolder <- function(path = cachePkgDir(), exact = FALSE) {
  if (is.null(path)) return(NULL)
  if (isFALSE(path)) return(NULL)
  if (isTRUE(exact)) return(path)

  path <- path[1]
  siteLibs <- normPathMemoise(strsplit(Sys.getenv("R_LIBS_SITE"), split = ":")[[1]])
  if (normPathMemoise(path) %in% siteLibs) return(path)

  if (interactive() && !endsWith(path, versionMajorMinor())) {
    ## R CMD check on R >= 4.2 uses a random tmp libpath; skip the
    ## R-version append there so the path the user-set libpath wins.
    file.path(path, versionMajorMinor())
  } else {
    path
  }
}


preparePkgNameToReport <- function(Package, packageFullName) {
  pkgsCleaned <- gsub(.grepTooManySpaces, " ", packageFullName)
  pkgsCleaned <- gsub(.grepTabCR, "", pkgsCleaned)
  pkgNameInPkgFullName <- unlist(Map(
    pkg = Package, pfn = packageFullName,
    function(pkg, pfn) grepl(pkg, pfn)
  ))
  Package[!pkgNameInPkgFullName] <- paste0(
    Package[!pkgNameInPkgFullName], " (",
    packageFullName[!pkgNameInPkgFullName], ")"
  )
  Package
}

splitGitRepo <- function(gitRepo, default = "PredictiveEcology", masterOrMain = NULL) {

  gitRepoOrig <- gitRepo
  # Can have version number --> most cases (other than SpaDES modules) just strip off
  gitRepo <- trimVersionNumber(gitRepo)
  hasVersionSpec <- gitRepo != gitRepoOrig

  ## The branch is whatever follows "@" -- it is not positional. This used to
  ## split on "/|@" and take the 3rd element as the branch, which is right for
  ## `Acct/Repo@branch` and `Acct/Repo@branch/subFolder` but wrong for
  ## `Acct/Repo/subFolder@branch`: there the 3rd element is the subFolder, and
  ## the real branch was silently discarded. Both spellings are in use.
  atPos    <- regexpr("@", gitRepo, fixed = TRUE)
  beforeAt <- ifelse(atPos > 0L, substr(gitRepo, 1L, atPos - 1L), gitRepo)
  afterAt  <- ifelse(atPos > 0L, substr(gitRepo, atPos + 1L, nchar(gitRepo)), "")

  leftSeg  <- strsplit(beforeAt, "/", fixed = TRUE)
  rightSeg <- strsplit(afterAt, "/", fixed = TRUE)

  repo <- lapply(leftSeg, function(x) x[[min(2L, length(x))]])
  acct <- lapply(leftSeg, function(x) if (length(x) > 1L) x[[1L]] else default)
  names(repo) <- names(acct) <- unlist(repo, use.names = FALSE)

  br <- lapply(rightSeg, function(r) if (length(r) && nzchar(r[[1L]])) r[[1L]] else "HEAD")
  names(br) <- names(repo)

  ## Anything past acct/repo on the left, plus anything past the branch on the
  ## right, is a path to a subfolder within the repo. Returned so callers need
  ## not re-parse the string; NA when there is none.
  subFolder <- Map(l = leftSeg, r = rightSeg, f = function(l, r) {
    extra <- c(if (length(l) > 2L) l[-seq_len(2L)], if (length(r) > 1L) r[-1L])
    if (length(extra)) paste(extra, collapse = "/") else NA_character_
  })
  names(subFolder) <- names(repo)

  vs <- lapply(leftSeg, function(x) list())
  names(vs) <- names(repo)
  if (any(hasVersionSpec)) {
    versionSpecs <- extractVersionNumber(gitRepoOrig[hasVersionSpec])
    inequs <- extractInequality(gitRepoOrig[hasVersionSpec])
    vs[hasVersionSpec] <- paste0("(", inequs, " ", versionSpecs, ")")
  }

  list(acct = acct, repo = repo, br = br, versionSpec = vs, subFolder = subFolder)
}

postInstallDESCRIPTIONMods <- function(pkgInstall, libPaths) {
  whGitHub <- which(pkgInstall$repoLocation %in% .txtGitHub)
  if (length(whGitHub)) {
    pkgGitHub <- pkgInstall[whGitHub]
    for (pk in pkgGitHub[installed %in% TRUE & grepl("OK|restart", installResult)]$Package) {
      if (is.null(pkgGitHub[["GitSubFolder"]]))
        set(pkgGitHub, NULL, "GitSubFolder", "")
      pkgGitHub[Package %in% pk, {
        file <- file.path(libPaths[1], Package, "DESCRIPTION")
        txt <- readLines(file)
        alreadyHasSHA <- grepl("Github|Remote", txt)
        leaveAlone <- FALSE
        if (any(alreadyHasSHA)) {
          if (any(grepl(SHAonGH, txt))) {
            leaveAlone <- TRUE
          }
        }

        if (isFALSE(leaveAlone)) {
          dups <- duplicated(vapply(strsplit(txt, split = "\\:"),
                                    function(x) x[[1]], FUN.VALUE = character(1)))
          if (any(dups)) {
            # Delete the first version of any duplicated entry -- i.e., take the more recent
            #   version
            dupsRev <- duplicated(vapply(strsplit(rev(txt), split = "\\:"),
                                      function(x) x[[1]], FUN.VALUE = character(1)))
            txtOut <- rev(rev(txt)[!dupsRev])
          }
          if (!exists("txtOut", inherits = FALSE)) {

            beforeTheseLines <- grep("NeedsCompilation:|Packaged:|Author:", txt)
            insertHere <- min(beforeTheseLines)
            sha <- SHAonGH
            newTxt <-
              paste0("RemoteType: github
RemoteHost: api.github.com
RemoteRepo: ", Repo, "
RemoteUsername: ", Account, "
RemoteRef: ", Branch, "
RemoteSha: ", sha, "
GithubRepo: ", Repo, "
GithubSubFolder: ", GitSubFolder, "
GithubUsername: ", Account, "
GithubRef: ", Branch, "
GithubSHA1: ", sha, "")
            newTxt <- strsplit(newTxt, split = "\n")[[1]]
            newTxt <- gsub("^ +", "", newTxt)
            txtOut <- c(txt[seq(insertHere - 1)], newTxt, txt[insertHere:length(txt)])
          }
          cat(txtOut, file = file, sep = "\n")
        }
      }]

    }
  }

  return(invisible())
}

#' @importFrom utils unzip
#' @inheritParams Require
downloadRepo <- function(gitRepo, subFolder, overwrite = FALSE, destDir = ".",
                         verbose = getOption("Require.verbose")) {
  dir.create(destDir, recursive = TRUE, showWarnings = FALSE)
  gr <- splitGitRepo(gitRepo)
  ar <- file.path(gr$acct, gr$repo)

  pkgName <- if (is.null(names(gitRepo))) gr$repo else names(gitRepo)

  repoFull <- file.path(destDir, pkgName)
  zipFileName <- normalizePath(paste0(repoFull, ".zip"), winslash = "/", mustWork = FALSE)
  masterMain <- c("main", "master")
  br <- if (any(gr$br %in% masterMain)) {
    # possibly change order -- i.e., put user choice first
    masterMain[rev(masterMain %in% gr$br + 1)]
  } else {
    gr$br
  }

  url <- file.path(githubDotCom, ar, "archive", paste0(br, ".zip"))
  out <- suppressWarnings(
    try(.downloadFileMasterMainAuth(url, destfile = zipFileName, need = "master"), silent = TRUE)
  )
  if (is(out, "try-error")) {
    return(out)
  }

  out <- lapply(zipFileName, function(zfn) unzip(zfn, exdir = destDir)) # unzip it

  de <- dir.exists(repoFull)
  if (any(de)) {
    if (isTRUE(overwrite)) {
      unlink(repoFull[de], recursive = TRUE)
    } else {
      stop(repoFull, " directory already exists. Use overwrite = TRUE if you want to overwrite it")
    }
  }
  # Finds the common component i.e., the base directory. This will have the SHA as part fo the filename; needs remving

  badDirname <- try(lapply(out, function(d) {
    unlist(lapply(out, function(x) {
      for (n in seq(nchar(x[1]))) {
        has <- all(startsWith(x, substr(x[1], 1, n)))
        if (any(isFALSE(has))) {
          break
        }
      }
      normPath(substr(x[1], 1, n - 1))
    }))
  }))
  if (is(badDirname, "try-error")) stop("Error 654; something went wrong with downloading & building the package")
  badDirname <- unlist(badDirname)
  if (!missing(subFolder))
    if (isTRUE(is.na(subFolder)) || isTRUE(is.null(subFolder))) {
      subFolder <- FALSE
    }

  newName <- unlist(Map(
    bad = badDirname, subFolder = subFolder, pkgName = pkgName,
    function(bad, subFolder, pkgName) {
      actualFolderName <- basename(gsub(subFolder, "", bad))
      if (!identical(actualFolderName, pkgName)) { # means the folder is not the pkgName e.g., mumin != MuMIn
        origOut <- normPath(out)
        outNP <- origOut
        newFolder <- dirname(bad)
        newFolder <- file.path(newFolder, pkgName)
        if (!isFALSE(subFolder)) { # get rid of subfolder for all files
          subFolderNP <- normPath(file.path(bad, subFolder))
          origOut <- grep(subFolderNP, origOut, value = TRUE)
          outNP <- grep(subFolderNP, origOut, value = TRUE)
          outNP <- gsub(subFolderNP, newFolder, outNP)
        } else {
          outNP <- gsub(bad, newFolder, outNP)
        }
        fileRenameOrMove(origOut, outNP) # do the rename
        newFolder
      }
    }
  ))
  unlink(zipFileName)
  messageVerbose(paste0(gitRepo, " downloaded and placed in ",
                        normalizePath(repoFull, winslash = "/"), collapse = "\n"),
                 verbose = verbose, verboseLevel = 2
  )
  return(normalizePath(repoFull))
}

getSHAfromGitHub <- function(acct, repo, br, verbose = getOption("Require.verbose")) {
  if (nchar(br) == 40) {
    return(br)
  }

  gitRefsURLOrig <- file.path(apiGithubDotCom, "repos", acct, repo, "git", "refs")
  if (missing(br)) {
    br <- "main"
  }
  if (identical(br, "HEAD")) {
    br <- "main"
  }
  masterMain <- c("main", "master")
  if (any(br %in% c(masterMain))) {
    # possibly change order -- i.e., put user choice first
    br <- masterMain[rev(masterMain %in% br + 1)]
  }

  for (ii in 1:40) {
    gitRefsURL <- paste0(gitRefsURLOrig, paste0("?per_page=100&page=", ii))
    tf <- file.path(RequireGitHubCacheDir(), paste0("listOfRepos_", acct, "@", repo))
    downloadNow <- TRUE
    if (file.exists(tf)) {
      if ((difftime(Sys.time(), file.info(tf)$mtime, units = "sec")) < 60) {
        downloadNow <- FALSE
      }
    }
    if (downloadNow) {
      mess <- capture.output(type = "message", out <-
        .downloadFileMasterMainAuth(gitRefsURL, destfile = tf, need = "master"))
    }
    fetf <- file.exists(tf)
    gitRefs <- if (fetf) try(suppressWarnings(readLines(tf)), silent = TRUE) else ""

    stopOnGitRefsFails(gitRefs, fetf, tf, acct, repo, br)
    # isNotFound <-  ((NROW(gitRefs) <= 5) && any(grepl("Not Found", gitRefs) ) ||
    #   (any(grepl("cannot open URL", gitRefs))) || identical(gitRefs, "") ||
    #     any(grepl("status.+403", gitRefs)))
    #
    # if (any(grepl("Bad credentials", gitRefs)) || isNotFound) {
    #   if (fetf) {
    #     unlink(tf)
    #   }
    #   if (isNotFound) {
    #     token <- getGitCredsToken()
    #     # mess <- character()
    #     if (is.null(token)) {
    #       # mess <- "GitHub repository not accessible does it need authentication? "
    #       stop(paste0(.txtGitHubMissingToken, "\n", .txtDidYouSpell))
    #     }
    #     stop(paste0(.txtDidYouSpell, " (", acct, "/", repo, "@", br, ")"))
    #   }
    #   stop(gitRefs)
    # }

    if (is(gitRefs, "try-error")) {
      if (isTRUE(any(grepl("cannot open the connection", gitRefs)))) {
        # means no internet
        setOfflineModeTRUE(verbose = verbose)
      }
      return(gitRefs)
    }
    if (length(gitRefs) > 1) {
      ## Seems to sometimes come out as individual lines; sometimes as one long concatenates string
      ##   Was easier to collapse the individual lines, then re-split
      gitRefs <- paste(gitRefs, collapse = "")
    }
    gitRefsSplit <- strsplit(gitRefs, "},")[[1]] # this splits onto separate lines

    gitRefsSplit2 <- strsplit(gitRefsSplit, ":")

    if (any(grepl("master|main|HEAD", unlist(br)))) {
      br2 <- masterOrMainFromGitRefs(gitRefsSplit2)
      if (length(br2) > 0)
        br <- br2
      else  {
        gitRefsURL <- paste0(gitRefsURLOrig, paste0("?per_page=100&page=", ii))
        unlink(tf)
        next
      }

      # br2 <- grep(unlist(gitRefsSplit2), pattern = "api.+heads/(master|main)", value = TRUE)
      # br <- gsub(br2, pattern = ".+api.+heads.+(master|main).+", replacement = "\\1")
    }

    for (branch in br) { # will be length 1 in most cases except master/main
      whHasBr <- which(vapply(gitRefsSplit2, function(xx) {
        any(grepl(paste0(".+refs/.+/+", branch, "\""), xx))
      }, FUN.VALUE = logical(1)))
      if (length(whHasBr) > 0) {
        break
      }
    }
    ## This will catch cases where the RequireGitHubCacheDir() doesn't have it,
    ##    but it is there (e.g., a new branch or new gitRefs)... this will deleted
    if (length(whHasBr) == 0) {
      if (ii %in% 1) {
        unlink(tf)
        next
      } else {
        stop(messageCantFind(br, acct, repo))
      }
    }
    break
  }

  gitRefsFinal <- gitRefsSplit2[[whHasBr]]
  shaLine <- grep("sha", gitRefsFinal) + 1
  shaLine <- strsplit(gitRefsFinal[shaLine], ",")[[1]][1]
  sha <- gsub(" *[[:punct:]]+(.+)[[:punct:]] *", "\\1", shaLine)
  sha
}

getSHAfromGitHubMemoise <- function(...) {
  pe <- pkgEnv()
  if (getOption("Require.useMemoise", TRUE)) {
    dots <- list(...)
    ret <- NULL
    ss <- match.call(definition = getSHAfromGitHub)
    uniqueID <- paste(lapply(ss[-1], eval, envir = parent.frame()), collapse = "_")

    # Use disk storage if in Require.offlineMode
    if (!exists(uniqueID, envir = pe[[.txtGetSHAfromGitHub]], inherits = FALSE) &&
        getOption("Require.offlineMode", FALSE)) {
      fn <- getSHAFromGitHubDBFilename()
      if (file.exists(fn)) {
        peList <- readRDS(fn)
        if (!is.null(peList[[uniqueID]])) {
          pe[[.txtGetSHAfromGitHub]][[uniqueID]] <- peList[[uniqueID]]
        }
      }
    }
    if (!exists(uniqueID, envir = pe[[.txtGetSHAfromGitHub]], inherits = FALSE)) {
      pe[[.txtGetSHAfromGitHub]][[uniqueID]] <- list()
    } else {
      whIdent <- unlist(lapply(pe[[.txtGetSHAfromGitHub]][[uniqueID]], function(x) identical(x$input, dots)))
      if (any(whIdent)) {
        ret <- pe[[.txtGetSHAfromGitHub]][[uniqueID]][[which(whIdent)]]$output
      }
    }
    if (is.null(ret)) { # Case where it doesn't exist in pe
      inputs <- data.table::copy(dots)
      ret <- getSHAfromGitHub(...)
      # Add it to the pe
      newObj <- list(pe[[.txtGetSHAfromGitHub]][[uniqueID]], list(input = inputs, output = ret))
      pe[[.txtGetSHAfromGitHub]][[uniqueID]] <- newObj
      fn <- getSHAFromGitHubDBFilename()
      peList <- as.list(pe[[.txtGetSHAfromGitHub]])
      if (length(fn)) { # this can be character() if cacheGetOptionCachePkgDir() is NULL
        if (!isTRUE(file.exists(fn))) {
          if (isFALSE(dir.exists(dirname(fn))))
            dir.create(dirname(fn), showWarnings = FALSE, recursive = TRUE)
          saveRDS(peList, file = fn)
        } else {
          peListExisting <- readRDS(file = fn)
          peList <- modifyList(peList, peListExisting)
          saveRDS(peList, file = fn)
        }
      }



    }
  } else {
    ret <- getSHAfromGitHub(...)
  }

  return(ret)
}

loadGitHubSHAsFromDisk <- function(verbose = getOption("Require.verbose")) {
  ret <- list()
  pe <- pkgEnv()
  if (!exists(.txtGetSHAfromGitHub, envir = pe, inherits = FALSE)) {
    fn <- getSHAFromGitHubDBFilename()
    if (isTRUE(file.exists(fn))) {
      out <- readRDS(fn)
      removeFile <- purgeBasedOnTimeSinceCached(out[[GitHubSHAonDiskCacheTime]])
      if (!removeFile) { # remove if 24 hours old
        # if (!exists(.txtGetSHAfromGitHub, envir = pe, inherits = FALSE))
        pe[[.txtGetSHAfromGitHub]] <- new.env()
        list2env(out, envir = pe[[.txtGetSHAfromGitHub]])
      } else {
        messageVerbose("Purging disk-backed pkgDep cache for GitHub SHA values; it has ",
                       "been more than ", defaultCacheAgeForPurge, ". Change this by setting ",
                       "Sys.getenv('R_AVAILABLE_PACKAGES_CACHE_CONTROL_MAX_AGE')",
                       verbose = verbose, verboseLevel = 2)
        unlink(fn)
      }
    }
  }

  ret <- getSHAFromPkgEnv()
  ret <- as.list(lapply(ret, function(x) x[[2]]$output))
  invisible(ret)
}

GitHubSHAonDiskCacheTime <- ".GitHubSHAonDiskCacheTime"

saveGitHubSHAsToDisk <- function(preShas) {
  pe <- pkgEnv()
  if (exists(.txtGetSHAfromGitHub, envir = pe, inherits = FALSE)) {
    obj <- getSHAFromPkgEnv()
    needSave <- if (missing(preShas)) {
      TRUE
    } else {
      length(setdiffNamed(as.list(lapply(obj, function(x) x[[2]]$output)), preShas)) > 0
    }
    fn <- getSHAFromGitHubDBFilename() # can return character() if RPackageCache is NULL; but here that is not possible
    if  (needSave && isTRUE(file.exists(fn))) {
      dd <- dirname(fn)
      if (!dir.exists(dd)) dir.create(dd, recursive = TRUE)
      obj[[GitHubSHAonDiskCacheTime]] <- format(Sys.time())
      out <- saveRDS(obj, fn)
    }
  }
}

getSHAFromPkgEnv <- function() {
  pe <- pkgEnv()
  as.list(pe[[.txtGetSHAfromGitHub]])
}


getSHAFromGitHubDBFilename <- function() {
  ## Require-private bookkeeping (not a package tarball) -- keep next to
  ## the legacy bookkeeping path so existing on-disk state is preserved.
  go <- .requirePkgInfoDir()
  if (!is.null(go) && nzchar(go))
    out <- file.path(go, paste0(.txtGetSHAfromGitHub, ".rds"))
  else
    out <- character()
  out
}



# .earliestRSPMDate <- "2015-06-06" # THIS WAS MRAN's DATE
.earliestRSPMDate <- "2017-10-10"
.latestRSPMDate <- Sys.Date() - 5

#' R versions
#'
#' Reference table of R versions and their release dates (2018 and later).
#'
#' Update this as needed using `rversions::r_versions()`:
#'
#' \verb{
#' # install.packages("rversions")
#' v = rversions::r_versions()
#' keep = which(as.Date(v$date, format = "%Y-%m-%d") >=
#'              as.Date("2018-01-01", format = "%Y-%m-%d"))
#' dput(v[keep, c("version", "date")])
#' }
rversions <- structure(
  list(
    version = c(
      "3.4.4", "3.5.0", "3.5.1", "3.5.2",
      "3.5.3", "3.6.0", "3.6.1", "3.6.2", "3.6.3", "4.0.0", "4.0.1",
      "4.0.2", "4.0.3", "4.0.4", "4.0.5", "4.1.0", "4.1.1", "4.1.2",
      "4.1.3", "4.2.0", "4.2.1"
    ),
    date = structure(c(
      1521101067, 1524467078,
      1530515071, 1545293080, 1552291489, 1556262303, 1562310303, 1576137903,
      1582963516, 1587711934, 1591427116, 1592809519, 1602313524, 1613376313,
      1617174315, 1621321522, 1628579106, 1635753912, 1646899538, 1650611141,
      1655967933
    ), class = c("POSIXct", "POSIXt"), tzone = "UTC")
  ),
  row.names = 108:128, class = "data.frame"
)

versionMajorMinor <- function(version = base::version) {
  if (!is(version, "simple.list")) {
    version <- strsplit(version, "[.]")[[1]]
    nams <- c("major", "minor", "revision")
    names(version) <- nams[seq_along(version)]
  }
  paste0(version[["major"]], ".", strsplit(version[["minor"]], "[.]")[[1]][1])
}


# Used inside internetExists
urlExists <- function(url) {
  con <- url(url)
  on.exit(try(close(con), silent = TRUE), add = TRUE)
  for (i in 1:5) {
    a <- try(suppressWarnings(readLines(con, n = 1)), silent = TRUE)
    try(close(con), silent = TRUE)
    ret <- if (is(a, "try-error")) FALSE else TRUE
    if (isTRUE(ret)) {
      break
    } else {
      Sys.sleep(0.1)
    }
  }
  ret
}

#' Internet Exists query
#'
#' Simple test for internet availability.
#'
#' @returns Logical. `TRUE` if internet is available, `FALSE` if not.
#'
#' @inheritParams Require
#' @param force If `TRUE`, probe even when `options("Require.checkInternet")`
#'   is `FALSE`. Used at points where we're about to do real work (e.g.,
#'   install dispatch) and a 2-second probe is cheap relative to the cost
#'   of failing late. Defaults to `FALSE` to preserve the no-probe default
#'   behaviour for all other call sites.
internetExists <- function(verbose = getOption("Require.verbose"),
                           force = FALSE) {
  if (!isTRUE(getOption("Require.offlineMode"))) {
    if (force || getOption("Require.checkInternet", FALSE)) {
      internetMightExist <- TRUE
      iet <- get0(.txtInternetExistsTime, envir = pkgEnv())
      checkNow <- TRUE
      if (!is.null(iet)) {
        if ((Sys.time() - getOption("Require.internetExistsTimeout", 30)) < iet) {
          internetMightExist <- get0(.txtInternetExists, envir = pkgEnv())
          checkNow <- FALSE
        }
      }
      if (checkNow) {
        opts2 <- options(timeout = 2)
        on.exit(options(opts2))
        pe <- pkgEnv()
        ue <- pe$internetExists <- urlExists("https://www.google.com")
        if (isFALSE(ue)) {
          internetMightExist <- FALSE

          # messageVerbose("\033[32mInternet does not appear to exist; proceeding anyway\033[39m",
          #                verbose = verbose, verboseLevel = 2
          # )
        }
        assign(.txtInternetExistsTime, Sys.time(), envir = pkgEnv())
        assign(.txtInternetExists, internetMightExist, envir = pkgEnv())
      }
      out <- internetMightExist
    } else {
      out <- TRUE
    }
  } else {
    out <- FALSE
  }
  out
}


#' A list of R packages that should likely be installed from Source, not Binary
#'
#' The list of R packages that `Require` installs from source on Linux, even if
#' the `getOptions("repos")` is a binary repository. This list can be updated by
#' the user by modifying the options `Require.spatialPkgs` or
#' `Require.otherPkgs`. Default "force source only packages" are visible with
#' `RequireOptions()`.
#' @param spatialPkgs A character vector of package names that focus on spatial analyses.
#' @param otherPkgs A character vector of package names that often
#'   require system specific compilation.
#' @param additional Any other packages to be added to the other 2 argument vectors
#' @export
#' @return
#' A sorted concatenation of the 3 input parameters.
sourcePkgs <- function(additional = NULL,
                       spatialPkgs = NULL,
                       otherPkgs = NULL) {
  .spatialPkgs <- getOption("Require.spatialPkgs")
  if (is.null(spatialPkgs)) {
    spatialPkgs <- .spatialPkgs
  }
  .otherPkgs <- getOption("Require.otherPkgs")
  if (is.null(otherPkgs)) {
    otherPkgs <- .otherPkgs
  }
  unique(sort(c(spatialPkgs, otherPkgs, additional)))
}

srcPackageURLOnCRAN <- "https://cloud.r-project.org/"

stripHTTPAddress <- function(addr) {
  addr <- gsub("https://(.+)", "\\1", unname(addr))
  addr <- gsub("/$", "", unname(addr))
  addr
}





masterMainHEAD <- function(url, need) {
  hasMasterMain <- grepl(masterMainGrep, url)
  hasMaster <- grepl(masterGrep, url)
  hasMain <- grepl(mainGrep, url)
  if (any(hasMasterMain) && need %in% masterMain) {
    # Good -- try both master and main
    br <- need
  } else if (any(hasMasterMain) && need %in% "HEAD") {
    # need change
    br <- "HEAD"
    url <- gsub(masterMainGrep, paste0("/", br, "\\1"), url)
  }
  HEADgrepForGitBranch <- paste0("/", paste("HEAD", collapse = "|"), "(/|\\.)")
  hasHEAD <- grepl(HEADgrepForGitBranch, url)
  if (any(hasHEAD) && need %in% masterMain) {
    br <- need
    url <- gsub(HEADgrepForGitBranch, paste0("/", br, "\\1"), url)
  }
  if (any(hasHEAD) && need %in% "HEAD") {
    br <- "HEAD"
  }
  if (any(hasMasterMain) && length(url) == 1) {
    newBr <- masterMain[hasMain + 1]
    url[[2]] <- gsub(masterMainGrep, paste0("/", newBr, "\\1"), url)
  }
  url
}

#' GITHUB_PAT-aware and `main`-`master`-aware download from GitHub
#'
#' Equivalent to `utils::download.file`, but taking the `GITHUB_PAT` environment
#' variable and using it to access the Github url.
#'
#' @inheritParams utils::download.file
#' @param need If specified, user can suggest which `master` or `main` or `HEAD` to
#'   try first. If unspecified, `HEAD` is used.
#' @inheritParams Require
#' @inheritParams messageVerbose
#' @importFrom utils download.file assignInMyNamespace
#' @return
#' This is called for its side effect, namely, the same as `utils::download.file`, but
#' using a `GITHUB_PAT`, it if is in the environment, and trying both `master` and
#' `main` if the actual `url` specifies either `master` or `main` and it does not exist.
#' @export
.downloadFileMasterMainAuth <- function(url, destfile, need = "HEAD",
                                        verbose = getOption("Require.verbose"), verboseLevel = 2) {
  ## Issue #140: R's default `options("timeout")` is 60s, which is too short
  ## for slow connections fetching a multi-MB GitHub source zip (the bare
  ## `download.file` calls below inherit it). Raise it for the duration of
  ## this call, with `Require.downloadTimeout` (seconds) as the user-facing
  ## knob. The legacy non-pak path is the only one that reaches here; under
  ## `Require.usePak = TRUE` pak's own libcurl downloader is used instead.
  oldTimeout <- getOption("timeout")
  options(timeout = max(oldTimeout, getOption("Require.downloadTimeout", 300L)))
  on.exit(options(timeout = oldTimeout), add = TRUE)

  if (!dir.exists(dirname(destfile)))
    silent <- checkPath(dirname(destfile), create = TRUE)
  hasMasterMain <- grepl(masterMainGrep, url)
  if (!all(hasMasterMain)) {
    if (length(url) > 1) stop("This function is not vectorized")
    if (length(url) != length(destfile))
      stop("destfile must be same length as url")
  }
  url <- masterMainHEAD(url, need) # makes 2

  # Authentication
  token <- checkForToken()
  # token <- NULL
  # usesGitCreds <- requireNamespace("gitcreds", quietly = TRUE) &&
  #   requireNamespace("httr", quietly = TRUE)
  # if (usesGitCreds) {
  #   token <- getGitCredsToken()
  # }
  if (is.null(token)) {
    ghp <- Sys.getenv("GITHUB_PAT")
    messageGithubPAT(ghp, verbose = verbose, verboseLevel = 0)
    if (nzchar(ghp)) {
      messageVerbose("For better security, user should use the newer way to store git credentials.",
                     "\nUsing a GITHUB_PAT environment variable will continue to work, but see: ",
                     "https://usethis.r-lib.org/articles/git-credentials.html", verbose = verbose + GitHubMessage)
      if (GitHubMessage >= 0)
        assignInMyNamespace("GitHubMessage", -10)
      url <- sprintf(paste0("https://%s:@", gsub("https*://", "", url)), ghp)
    }
  }

  urls <- url
  urls <- split(urls, hasMasterMain)
  outNotMasterMain <- outMasterMain <- character()

  ret <- withCallingHandlers({
    for (i in 1:2) {
      if (!is.null(urls[["FALSE"]])) {
        outNotMasterMain <-
          Map(URL = urls[["FALSE"]], MoreArgs = list(df = destfile), function(URL, df) {
            for (tryNum in 1:2) {
              if (!isTRUE(getOption("Require.offlineMode"))) {

                if (is.null(token)) {
                  tryCatch(
                    {
                      if (!dir.exists(dirname(df))) dir.create(dirname(df), recursive = TRUE)
                      download.file(URL, destfile = df, quiet = TRUE) # need TRUE to hide ghp
                    },
                    error = function(e) {
                      if (is.null(token))
                        e$message <- stripGHP(ghp, e$message)
                      if (tryNum > 1)
                        messageVerbose(e$message, verbose = verbose)
                    })
                } else {
                  a <- try(GETWauthThenNonAuth(url, token, verbose = verbose))
                  if (is(a, "try-error")) {
                    if (any(grepl("Could not resolve host", a))) {
                      warning(a)
                      next
                    }
                  }
                  # a <- httr::GET(url, httr::add_headers(Authorization = token))
                  # if (grepl("Bad credentials", a) || grepl("404", a$status_code))
                  #   a <- httr::GET(url, httr::add_headers())
                  data <- httr::content(a, "raw")
                  writeBin(data, df)
                }
                if (file.exists(df))
                  break
                if (is.null(token))
                  URL <- stripGHP(ghp, URL) # this seems to be one of the causes of failures -- the GHP sometimes fails
              }
            }
            return(invisible())
          })
      }
      if (!is.null(urls[["TRUE"]])) { # should be sequential because they are master OR main
        for (wh in seq(urls[["TRUE"]])) {
          if (!isTRUE(getOption("Require.offlineMode"))) {
            if (is.null(token)) {
              outMasterMain <- try(download.file(urls[["TRUE"]][wh], destfile = destfile, quiet = TRUE), silent = TRUE)
            } else {
              outMasterMain <- try(silent = TRUE, {
                a <- GETWauthThenNonAuth(urls[["TRUE"]][wh], token, verbose = verbose)
                # a <- httr::GET(urls[["TRUE"]][wh], httr::add_headers(Authorization = token))
                if (grepl("404", httr::http_status(a)$message))
                  stop()
                data <- httr::content(a, "raw")
                writeBin(data, destfile)
              })
              if (is.null(outMasterMain)) outMasterMain <- 0
            }
          }

          if (!is(outMasterMain, "try-error")) {
            namForOut <- if (is.null(token)) {
              stripGHP(ghp, urls[["TRUE"]][wh])
            } else {
              urls[["TRUE"]][wh]
            }
            names(outMasterMain) <- namForOut
            break
          }
        }
      }
      ret <- c(outNotMasterMain, outMasterMain)
      if (!any(unlist(lapply(ret, is, "try-error")))) {
        break
      }
      Sys.sleep(0.5)
    }
    ret
  },
  warning = function(w) {
    setOfflineModeTRUE(verbose = verbose)
    # strip the ghp from the warning message
    if (is.null(token))
      w$message <- stripGHP(ghp, w$message)
    invokeRestart("muffleWarning")
  },
  error = function(e) {
    # strip the ghp from the message
    if (is.null(token))
      e$message <- stripGHP(ghp, e$message)
    stop(e)
  })

  ret
}

stripGHP <- function(ghp, mess) {
  if (!missing(ghp))
    mess <- gsub(paste0(ghp, ".*@"), "", mess)
  mess
}

messageGithubPAT <- function(ghp, verbose = verbose, verboseLevel = 0) {
  if (nzchar(ghp)) {
    if (is.null(get0(.txtPkgHasGHP, envir = pkgEnv()))) {
      assign(.txtPkgHasGHP, TRUE, envir = pkgEnv())
      messageVerbose("Using GITHUB_PAT to access files on GitHub",
                     verboseLevel = 0, verbose = verbose
      )
    }
  }
}

notInArchives <- "Not in Archives"


masterMain <- c("main", "master")
masterMainGrep <- paste0("/", paste(masterMain, collapse = "|"), "(/|\\.)")
masterGrep <- paste0("/", "master", "(/|\\.)")
mainGrep <- paste0("/", "main", "(/|\\.)")

extractPkgNameFromWarning <- function(x) {

  if (any(grepl(.txtMsgIsInUse, x)) || # "in use"
      any(grepl(.txtInstallationPkgFailed, x))) { # "installation of 2 packages failed:"
    out <- NULL
    if (isWindows()) {
      aa <- strsplit(x, "\\'")
      out <- lapply(aa, function(y) {
        wh <- grep(", ", y)
        wh <- c(1, wh)
        y[c(wh + 1)]
      })
    }
    if (is.na(out) || !isWindows()) {
      aa <- strsplit(x, "\u2019|\u2018")[[1]]
      aa <- grep(.txtInstallationPkgFailed, aa, invert = TRUE, value = TRUE)
      aa <- grep("package|is in use|failed", aa, invert = TRUE, value = TRUE)
      out <- grep(", ", aa, value = TRUE, invert = TRUE)
    }

    out <- unlist(out)
  } else {
    out <- gsub(".+\u2018(.+)_.+\u2019.*", "\\1", x) # those two escape characters are the inverted commas
    out <- gsub(".+\u2018(.+)\u2019.*", "\\1", out)
    out <- gsub("^.+\\'(.+)\\'.+$", "\\1", out)
    out <- gsub(".+\u2018(.+)\u2019.+", "\\1", out) # package XXX is in use and will not be installed
  }

  if (isTRUE(any(grepl(.txtCannotOpenFile, x)))) {
    outs <- strsplit(out, split = "/|\\\\")
    out <- sapply(outs, function(x) x[length(x) - 1])
  }
  out
}

availablePackagesCachedPath <- function(repos, type) {
  ## Require's own available.packages snapshot -- bookkeeping, not a
  ## package tarball, so it lives in .requirePkgInfoDir() not pak's cache.
  file.path(.requirePkgInfoDir(),
            paste0(gsub("https|[:/]", "", repos), collapse = "/"),
            type, "availablePackages.rds")
}

installPackagesWithQuiet <- function(ipa, verbose) {
  if (isWindows())
    messageVerbose("  -- ", .txtInstallingColon,"\n", verbose = verbose, appendLF = FALSE)
  if (isWindows() && identical(ipa$type, "source") &&
      getOption("Require.installPackagesSys") == 0) {
    op <- options(Ncpus = 1)
    on.exit(options(op), add = TRUE)
  }

  if (isTRUE(length(ipa$type) > 1))
    ipa$type <- ipa$type[2]

  if (getOption("Require.installPackagesSys") &&
      requireNamespace("sys", quietly = TRUE)){
    for (i in 1:1) {
      anyFailed <- NULL
      out <- #try(
        sysInstallAndDownload(ipa, splitOn = "pkgs", tmpdir = ipa$destdir,
                                   doLine = "outfiles <- do.call(install.packages, args)",
                                   verbose = verbose)
        #)
      if (file.exists(out)) {
        txt <- readLines(out)
        anyFailed <- grep(.txtInstallationPkgFailed, txt)
      }

      if (length(anyFailed) == 0)
        break

      pkgName <- extractPkgNameFromWarning(txt[anyFailed])
      if (any(is.na(pkgName)))
        pkgName <- extractPkgNameFromWarning(paste(txt[anyFailed:(anyFailed + 1)], collapse = ""))
      messageVerbose("Failed installation for: ", paste(pkgName, collapse = ", "),
                     "\nTrying again ... ", verbose = verbose)
      ipa$pkgs <- pkgName
      ipa$available <- ipa$available[ipa$available[, "Package"] %in% pkgName, , drop = FALSE]
    }
  } else {

    if (isTRUE(ipa$quiet)) {
      messSupp2 <- capture.output({
        messSupp <- capture.output(type = "message", {
          out <- do.call(install.packages, ipa)
        })
      })
    } else {
      out <- do.call(install.packages, ipa)
    }
  }
  return(out)
}

#' @importFrom utils remove.packages
checkHEAD <- function(pkgDT) {
  if (is.null(pkgDT[[hasHEADtxt]])) {
    data.table::alloc.col(pkgDT)
    set(pkgDT, NULL, hasHEADtxt, grepl(HEADgrepWithParentheses, pkgDT$packageFullName))
  }
  pkgDT
}

packageFullName <- function(pkgDT) {
  inequality <- if (is.null(pkgDT[["inequality"]])) "==" else pkgDT[["inequality"]]
  if (all(colnames(pkgDT) %in% c("GithubRepo", "GithubUsername"))) {
    ifelse(!is.na(pkgDT$GithubRepo) & nzchar(pkgDT$GithubRepo),
           paste0(pkgDT$GithubUsername, "/", pkgDT$Package, "@", pkgDT$GithubSHA1),
           paste0(pkgDT$Package, ifelse(is.na(pkgDT$Version) | is.na(pkgDT$inequality),
                                        "", paste0(" (", inequality, pkgDT$Version, ")")
           ))
    )
  } else {
    ifelse(!is.na(pkgDT$Repo) & nzchar(pkgDT$Repo),
           paste0(pkgDT$Account, "/", pkgDT$Package, "@", pkgDT$SHAonGH),
           paste0(pkgDT$Package, ifelse(is.na(pkgDT$Version) | is.na(pkgDT$inequality),
                                        "", paste0(" (", inequality, pkgDT$Version, ")")
           ))
    )
  }
}

gitHubFileUrl <- function(hasSubFolder, Branch, GitSubFolder, Account, Repo, filename) {
  if (any(hasSubFolder, na.rm = TRUE)) {
    isHash <- grepl("[[:alnum:]]{40}", Branch)
    if (!grepl("/", Branch) && !isHash)
      Branch <- paste0(Branch, "/", GitSubFolder)
  }
  file.path(rawGithubDotCom, Account, Repo, Branch, filename, fsep = "/")
}


setOfflineModeTRUE <- function(verbose = getOption("Require.verbose"),
                               force = FALSE) {
  if (!isTRUE(getOption("Require.offlineMode"))) {
    if (!internetExists(force = force)) {
      options(
        "Require.offlineMode" = TRUE,
        "Require.offlineModeSetAutomatically" = TRUE
      )
      messageVerbose("Internet appears to be unavailable; setting options('Require.offlineMode' = TRUE)",
                     verbose = verbose)
    }
  }
}

checkAutomaticOfflineMode <- function() {
  if (getOption("Require.offlineModeSetAutomatically", FALSE)) {
    options(
      "Require.offlineModeSetAutomatically" = NULL,
      Require.offlineMode = FALSE
    )
  }
}

isRstudioServer <- function () {
  isRstudioServer <- FALSE
  if (isRstudio()) {
    rsAPIFn <- get(".rs.api.versionInfo", as.environment("tools:rstudio"))
    versionInfo <- rsAPIFn()
    if (!is.null(versionInfo)) {
      isRstudioServer <- identical("server", versionInfo$mode)
    }
  }
  isRstudioServer
}

isRstudio <- function() {
  isTRUE("tools:rstudio" %in% search())
}

installPackageVerbose <- function(verbose, verboseLevel = 1) {
  verbose >= verboseLevel && verbose < 5
}

RequireGitHubCacheFile <- function(pkgDT, filename) {
  theDir <- RequireGitHubCacheDir(create = TRUE)
  # checkPath(theDir, create = TRUE)
  destFile <- if(is.null(pkgDT$shas)) pkgDT$Branch else pkgDT$shas
  destFile <- paste0(pkgDT$Account, "_", pkgDT$Package, "_", destFile)
  destFile <- file.path(theDir, paste0(destFile, "_", filename))
}


rmEmptyFiles <- function(files, minSize = 100) {
  notNAs <- is.na(files) %in% FALSE
  alreadyExists <- rep(FALSE, length(files))
  if (any(notNAs)) {
    alreadyExists[notNAs] <- file.exists(files[notNAs])
    if (any(alreadyExists[notNAs])) {
      fs <- file.size(files[notNAs][alreadyExists])
      tooSmall <- fs < minSize
      if (any(tooSmall %in% TRUE)) {
        unlink(files[alreadyExists[notNAs][which(tooSmall)]])
        alreadyExists[notNAs][alreadyExists] <- tooSmall %in% FALSE
      }
    }
  }

  alreadyExists
}


#' GET a URL with a GitHub token, falling back to an unauthenticated request
#'
#' Signs the request with `httr::add_headers(Authorization = token)` when a
#' token is supplied, and retries without credentials if GitHub rejects them or
#' returns a 404. `token` must carry the `"token "` prefix that
#' [getGitCredsToken()] produces -- a bare PAT authenticates nothing.
#'
#' Exported for the SpaDES packages, which query the GitHub API for module
#' repositories and need the same authentication behaviour as `Require`. Not
#' part of the advertised API.
#'
#' @param url The URL to GET.
#' @param token A GitHub token of the form `"token <pat>"`, or `NULL` for an
#'   unauthenticated request.
#' @param verbose Numeric or logical, controlling messaging verbosity.
#'
#' @return The `httr` response object.
#'
#' @export
#' @keywords internal
#' @rdname GETWauthThenNonAuth
GETWauthThenNonAuth <- function(url, token, verbose = getOption("Require.verbose")) {
  if (is.null(token)) {
    a <- httr::GET(url)
  } else {
    a <- httr::GET(url, httr::add_headers(Authorization = token))
  }
  if (grepl("Bad credentials", a) || grepl("404", httr::http_status(a)$message)) {
    if (grepl("Bad credentials", a)) messageVerbose(red("Git credentials do not work for this url: ", url,
                                             "\nAre they expired?"), verbose = verbose)
    a <- httr::GET(url, httr::add_headers())
  }
  a
}



available.packagesWithCallingHandlers <- function(repo, type, verbose = getOption("Require.verbose")) {
  ignore_repo_cache <- FALSE
  for (attmpt in 1:2) {
    warns <- character()
    withCallingHandlers(
      out <- try(available.packages(repos = repo, type = type,
                                    ignore_repo_cache = ignore_repo_cache)),
      warning = function(w) {
        if (!grepl(.txtBenignAvailPkgsWarns, w$message)) {
          warns <<- c(warns, w$message)
        }
        invokeRestart("muffleWarning")
      })

    accessFail <- is(out, "try-error") || any(grepl(.txtUnableToAccessIndex, warns))
    if (accessFail && attmpt == 1L) {
      # https://stackoverflow.com/a/76684292/3890027
      enableSSLWorkaround()
      ignore_repo_cache <- TRUE
      next
    }
    break
  }

  stillFailing <- is(out, "try-error") || any(grepl(.txtUnableToAccessIndex, warns))
  if (stillFailing) setOfflineModeTRUE(verbose = verbose)

  otherwarns <- grep(.txtUnableToAccessIndex, warns, invert = TRUE, value = TRUE)
  for (w in otherwarns) warning(w, call. = FALSE)

  out
}



masterOrMainFromGitRefs <- function(gitRefsSplit2) {
  br2 <- grep(unlist(gitRefsSplit2), pattern = "api.+heads/(master|main)", value = TRUE)
  br <- gsub(br2, pattern = ".+api.+heads.+(master|main).+", replacement = "\\1")
  br
}

#' Find a GitHub token, from the git credential store or the environment
#'
#' `gitcreds::gitcreds_get()` reads the *git credential store* only; it does not
#' consult `GITHUB_PAT` or `GITHUB_TOKEN`. CI runners routinely set the
#' environment variable and configure no credential helper, so relying on
#' gitcreds alone left API calls unauthenticated and subject to GitHub's
#' 60-request/hour per-IP limit -- which surfaces as intermittent HTTP 403s
#' spread across whichever job happens to exhaust the quota.
#'
#' The credential store still wins when it has something, so behaviour on a
#' developer machine is unchanged; the environment is only a fallback.
#'
#' Exported for the SpaDES packages, which authenticate GitHub API calls the
#' same way. Not part of the advertised API.
#'
#' @return The `"token <pat>"` string [GETWauthThenNonAuth()] expects, or `NULL`.
#' @export
#' @keywords internal
#' @rdname getGitCredsToken
getGitCredsToken <- function() {
  token <- gitcredsToken()
  if (is.null(token)) {
    token <- envToken()
  }
  token
}

## Kept as its own function so tests can drive the "no credential store" case
## without mocking another package's bindings.
gitcredsToken <- function() {
  token <- tryCatch(
    gitcreds::gitcreds_get(use_cache = FALSE),
    error = function(e) NULL
  )
  if (!is.null(token)) {
    token <- paste0("token ", token$password)
  }
  token
}

envToken <- function() {
  pat <- Sys.getenv("GITHUB_PAT", unset = "")
  if (!nzchar(pat)) {
    pat <- Sys.getenv("GITHUB_TOKEN", unset = "")
  }
  if (nzchar(pat)) paste0("token ", pat) else NULL
}


readLinesWithHandlers <- function(fff) {
  if (length(fff) == 1) {
    withCallingHandlers(
      lines <- try(readLines(fff), silent = TRUE),
      warning = function(w) {
        if (grepl(
          paste('incomplete final line found on', 'cannot open file', 'cannot open the connection', sep = "|"),
          w$message))
          invokeRestart("muffleWarning")
      }
    )

    # lines <- try(readLines(fff), silent = TRUE)
    if (is(lines, "try-error")) {
      warning(lines)
      lines <- character()
    }
    any404 <- suppressWarnings(any(grepl("404: Not Found", lines)))
    if (isTRUE(any404))
      lines <- character()
  } else {
    lines <- fff
  }
  lines
}



.downloadFileMasterMainAuthWithHandlers <- function(Account, Package, Branch, packageFullName,
                                                    filename, url, destFile, verbose) {
  messageVerbose(Package, "@", Branch, " downloading ", filename, verbose = verbose - 1)
  ret <- NA
  dl <- try(.downloadFileMasterMainAuth(unique(url)[1], unique(destFile)[1],
                                        need = "master",
                                        verbose = verbose
  ))
  ret <- if (!is(dl, "try-error")) {
    destFile
  } else {
    if (!isTRUE(urlExists(unique(url)[1])))
      if (!isTRUE(urlExists("https://www.google.com"))) {
        setOfflineModeTRUE(verbose = verbose)
      }
    NA
  }

  stopNotAvailable <- FALSE
  failDoesntExist <- seq_along(packageFullName)
  suggs <- ""
  if (any(file.exists(destFile))) {
    fs <- file.size(destFile)
    fsTooSmallLikelyError <- fs < 100
    if (any(fsTooSmallLikelyError)) {
      checkFile <- suppressWarnings(lapply(destFile[fsTooSmallLikelyError], readLines))
      failDoesntExist <- sapply(checkFile, grepl, pattern = "404")
      if (any(failDoesntExist)) {
        suggs <- if (nchar(Branch) != 40) {
          offerGitHubSuggestions(account = Account, Package)
        } else {
          ""
        }
        token <- checkForToken()
        if (is.null(token))
          warning("There is no github token")
        stopNotAvailable <- TRUE
      }
    }
  } else {
    stopNotAvailable <- TRUE
  }
  if (isTRUE(stopNotAvailable)) {
    stop("The following repository does not seem to exist: \n",
         paste(packageFullName[failDoesntExist], collapse = "\n"),
         "\n", .txtDidYouSpell,
         if (isTRUE(nzchar(suggs))) paste0("\nDid you mean one of:\n",
                                   paste(suggs, collapse = "\n"))
    )
  }

  # if (grepl("404", httr::http_status(a)$message)) {
  #   offerGitHubSuggestions(account, repo)
  #   stop("The following repository does not seem to exist: \n",
  #        paste(pkgDTNotLocal$packageFullName[has404], collapse = "\n"),
  #        "\n", .txtDidYouSpell,
  #        "\nDid you mean one of:\n",
  #        paste(suggs, collapse = "\n")
  #   )
  # }

  ret
}

offerGitHubSuggestions <- function(account, repo) {
  # apiGithubDotCom <- "https://api.github.com"
  # account <- "PredictiveEcology"
  url <- paste0(apiGithubDotCom, "/users/", account, "/repos?per_page=500")
  names(url) <- account
  tf <- tempfile()
  out <- try(.downloadFileMasterMainAuth(url, destfile = tf, need = "master"))

  fetf <- file.exists(tf)
  gitRefs <- if (fetf) try(suppressWarnings(readLines(tf)), silent = TRUE) else ""
  stopOnGitRefsFails(gitRefs, fetf, tf, account, repo, br = NULL)
  # isNotFound <-  ((NROW(gitRefs) <= 5) && any(grepl("Not Found", gitRefs) ) ||
  #                   (any(grepl("cannot open URL", gitRefs))) || identical(gitRefs, "") ||
  #                   any(grepl("status.+403", gitRefs)))
  #
  # if (any(grepl("Bad credentials", gitRefs)) || isNotFound) {
  #   if (fetf) {
  #     unlink(tf)
  #   }
  #   if (isNotFound) {
  #     token <- getGitCredsToken()
  #     # mess <- character()
  #     if (is.null(token)) {
  #       # mess <- "GitHub repository not accessible does it need authentication? "
  #       stop(paste0(.txtGitHubMissingToken, "\n", .txtDidYouSpell))
  #     }
  #     stop(paste0(.txtDidYouSpell, " (", acct, "/", repo, "@", br, ")"))
  #   }
  #   stop(gitRefs)
  # }

  rl <- readLines(tf)
  poss <- grep(paste0("git_url.+"), rl, value = TRUE)
  poss2 <- agrep(repo, poss, value = TRUE)
  poss3 <- grep(file.path(account, paste0(repo, ".git")), poss2, value = TRUE, invert = TRUE)
  gsub(paste0("^.+(", file.path(account, repo), ".*).git.+"), "\\1", poss3)
}



githubDotCom <- "https://github.com"
rawGithubDotCom <- "https://raw.githubusercontent.com"
apiGithubDotCom <- "https://api.github.com"


checkForToken <- function() {
  token <- NULL
  usesGitCreds <- requireNamespace("gitcreds", quietly = TRUE) &&
    requireNamespace("httr", quietly = TRUE)
  if (usesGitCreds) {
    token <- getGitCredsToken()
  }
  token
}


stopOnGitRefsFails <- function(gitRefs, fetf, tf, acct, repo, br) {
  isNotFound <-  ((NROW(gitRefs) <= 5) && any(grepl("Not Found", gitRefs) ) ||
                    (any(grepl("cannot open URL", gitRefs))) || identical(gitRefs, "") ||
                    any(grepl("status.+403", gitRefs)))

  if (any(grepl("Bad credentials", gitRefs)) || isNotFound) {
    if (fetf) {
      unlink(tf)
    }
    if (isNotFound) {
      token <- getGitCredsToken()
      # mess <- character()
      if (is.null(token)) {
        # mess <- "GitHub repository not accessible does it need authentication? "
        stop(paste0(.txtGitHubMissingToken, "\n", .txtDidYouSpell))
      }
      stop(paste0(.txtDidYouSpell, " (", acct, "/", repo, ifelse(is.null(br), "", paste0("@", br)), ")"))
    }
    stop(gitRefs)
  }
}


isGitCommitHash <- function(Branch) {
  grepl("[[:alnum:]]{40}", Branch)
}


enableSSLWorkaround <- function() {
  prev <- Sys.getenv("R_LIBCURL_SSL_REVOKE_BEST_EFFORT")
  Sys.setenv(R_LIBCURL_SSL_REVOKE_BEST_EFFORT = TRUE)
  on.exit2({
    if (nzchar(prev))
      Sys.setenv(R_LIBCURL_SSL_REVOKE_BEST_EFFORT = prev)
    else
      Sys.unsetenv("R_LIBCURL_SSL_REVOKE_BEST_EFFORT")
  }, add = TRUE)
  invisible()
}
