#' Save a whole `simList` object to disk
#'
#' Saving a `simList` may not work using the standard approaches
#' (e.g., `save`, `saveRDS`, and `qs2::qs_save`).
#' There are 2 primary reasons why this doesn't work as expected:
#' the `activeBindings` that are in place within modules
#' (these allow the `mod` and `Par` to exist), and file-backed objects,
#' such as `SpatRaster` and `Raster*`.
#' Because of these, a user should use `saveSimList` and `loadSimList`.
#' These will save the object and recover the object using the `filename` supplied,
#' if there are no file-backed objects.
#' If there are file-backed objects, then it will save an archive
#' (default is `.tar.gz` using the `archive` package for non-Windows and [zip()]
#' if using Windows, as there is currently an unidentified bug in `archive*` on Windows).
#' The user does not need to specify the filename any differently,
#' as the code will search based on the filename without the file extension.
#'
#' @details
#' There is a family of 2 functions that are mutually useful for saving and
#' loading `simList` objects and their associated files (e.g., file-backed
#' `Raster*`, `inputs`, `outputs`, `cache`) [saveSimList()], [loadSimList()].
#'
#' The `sim@.xData$._sim` slot (a circular reference used internally during a
#' running simulation) is removed before saving to avoid redundant data.
#' It is not needed for a saved/restored `simList`.
#'
#' Additional arguments may be passed via `...`, including:
#' - `files`: logical indicating whether files should be included in the archive.
#'            if `FALSE`, will override `cache`, `inputs`, `outputs`, setting them to `FALSE`.
#' - `symlinks`: a named list of paths corresponding to symlinks, which will be used to substitute
#'               normalized absolute paths of files.
#'               Names should correspond to the names in `paths()`;
#'               values should be project-relative paths.
#'               E.g., `list(cachePath = "cache", inputPath = "inputs", outputPath = "outputs")`.
#'
#' @param sim Either a `simList` or a character string of the name
#'        of a `simList` that can be found in `envir`.
#'        Using a character string will assign that object name to the saved
#'        `simList`, so when it is recovered it will be given that name.
#'
#' @param envir If `sim` is a character string, then this must be provided.
#'        It is the environment where the object named `sim` can be found.
#'
#' @param filename Character string with the path for saving `simList` to or
#'   reading the `simList` from. Currently, only `.rds` and `.qs2` file types are supported.
#'
#' @param outputs Logical. If `TRUE`, all files identified in
#'    `outputs(sim)` will be included in the zip.
#'
#' @param inputs Logical. If `TRUE`, all files identified in
#'    `inputs(sim)` will be included in the zip.
#'
#' @param cache Logical. Not yet implemented. If `TRUE`, all files in `cachePath(sim)`
#'    will be included in the archive.
#'    Defaults to `FALSE` as this could be large, and may include many out of date elements.
#'    See Details.
#'
#' @param projectPath Should be the "top level" or project path for the `simList`.
#'    Defaults to `getwd()`. All other paths will be made relative with respect to
#'    this if nested within this.
#' @param files Logical. Should all the files in the optional `outputs`, `inputs`,
#'   `cache` be saved. If this is `TRUE`, then the resulting `filename` will be
#'   silently converted to an archive file with the appropriate extension e.g.,
#'   `.zip` or `.tar.gz`. This will automatically be `TRUE` if any of the `outputs`,
#'   `inputs` or `cache` are `TRUE`. Setting this to `FALSE` will turn off the
#'   saving of files specified in `inputs(sim)`, `outputs(sim)` or the cache.
#'
#' @param lazy Logical. If `TRUE`, the user objects in `sim@.xData` are saved
#'   into a sibling lazy-load DB (a `<filename>_xData.rdx`/`.rdb` pair built
#'   by `tools:::makeLazyLoadDB()`) alongside the shell `simList` file,
#'   rather than monolithically. [loadSimList()] detects this layout
#'   automatically and restores the objects via [lazyLoad()], materializing
#'   each one only on first access. Defaults to `FALSE`.
#' @param ... Additional arguments. See Details.
#'
#' @return
#' Invoked for side effects of saving both a `.qs2` (or `.rds`) file,
#' and a compressed archive (one of `.tar.gz` if using non-Windows OS or `.zip` on Windows).
#'
#' @aliases saveSim
#' @export
#' @importFrom fs path_common
#' @importFrom qs2 qs_save
#' @importFrom stats runif
#' @importFrom reproducible makeRelative .wrap
#' @importFrom Require messageVerbose
#' @importFrom tools file_ext
#' @importFrom utils modifyList
#' @rdname saveSimList
#' @seealso [loadSimList()]
saveSimList <- function(sim, filename, projectPath = getwd(),
                        outputs = TRUE, inputs = TRUE, cache = FALSE, envir,
                        files = TRUE, ..., lazy = FALSE) {
  checkSimListExts(filename)

  dots <- list(...)

  ## user can explicitly override archiving files if FALSE
  if (isFALSE(files)) {
    files <- cache <- inputs <- outputs <- FALSE
  } else {
    files <- TRUE
  }

  symlinks <- dots$symlinks

  verbose <- if (is.null(dots$verbose)) {
    if (is.null(dots$quiet)) {
      getOption("reproducible.verbose")
    } else {
      !isTRUE(dots$quiet)
    }
  } else {
    isTRUE(dots$verbose)
  }

  # clean up misnamed arguments
  if (!is.null(dots$fileBackedDir)) {
    if (is.null(filebackedDir)) {
      filebackedDir <- dots$fileBackedDir
      dots$fileBackedDir <- NULL
    }
  }

  if (!is.null(dots$filebackend))
    if (is.null(dots$fileBackend)) {
      dots$fileBackend <- dots$filebackend
      dots$filebackend <- NULL
    }

  if (!is.null(dots$fileBackend)) {
    warning(warnDeprecFileBacked("fileBackend"))
    fileBackend <- 0
  }

  if (!is.null(dots$filebackedDir)) {
    warning(warnDeprecFileBacked("filebackedDir"))
    fileBackend <- 0
  }

  if (is.character(sim)) {
    simName <- sim
    sim <- get(simName, envir = envir)
  }

  ## Break every pass-by-reference link into the caller's sim. simList
  ## state lives almost entirely in environments (@.xData and the nested
  ## .mods / .modObjs / .objects envs), and the subsequent rebindings
  ## (._randomSeed/._rng.kind, .wrapResiliently's per-object Path wrapping,
  ## paths() rewrite, lazy rm) would otherwise mutate the caller's state.
  ## We avoid reproducible::Copy(sim) here because its SpatRaster path
  ## eagerly duplicates backing files (filebackedDir = NULL is documented
  ## but not honored by the ANY method). .cloneSimEnvs recursively clones
  ## every environment but leaves leaf values (SpatRasters, data.tables,
  ## etc.) alone — saveSimList rebinds names, not nested object internals,
  ## so leaf-pointer sharing is harmless.
  sim <- .cloneSimEnvs(sim)

  if (!exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) tmp <- runif(1)
  sim@.xData$._randomSeed <- get(".Random.seed", envir = .GlobalEnv, inherits = FALSE)
  sim@.xData$._rng.kind <- RNGkind()

  messageVerbose("Saving simList object to file '", filename, "'.", verbose = verbose)

  if (exists("simName", inherits = FALSE)) {
    tmpEnv <- new.env(parent = emptyenv())
    assign(simName, sim, envir = tmpEnv)
    sim <- get(simName, envir = tmpEnv)
  }

  ## Pre-wrap file-backed objects one-by-one so a single inaccessible backing file
  ## does not abort the entire save; failed objects are saved as NULL with a warning.
  sim <- .wrapResiliently(sim)
  sim <- .wrap(sim, cachePath = NULL, paths = paths(sim)) # wrap remaining / non-file-backed
  sim@.xData$._sim <- NULL # remove circular reference; sim is already a Copy here
  sim@current <- list() # it is presumed that this event should be considered finished prior to saving

  if (isTRUE(files)) {
    fns <- Filenames(sim)
    empties <- nchar(fns) == 0
    if (any(empties)) {
      fns <- fns[!empties]
    }
  }

  ## This forces it to be qs2 (if not rds) instead of zip or tar.gz
  if (tools::file_ext(filename) != "rds") {
    filename <- archiveConvertFileExt(filename, "qs2")
  }

  origPaths <- paths(sim)
  if (is.null(symlinks)) {
    paths(sim) <- origPaths |>
      relativizePaths(projectPath) |>
      as.list()
  } else {
    paths(sim) <- origPaths |>
      modifyList(symlinks) |>
      relativizePaths(projectPath) |>
      as.list()
  }

  if (isTRUE(lazy)) {
    ext <- tools::file_ext(filename)
    lazyBase <- paste0(tools::file_path_sans_ext(filename), "_xData")
    lazyDbFiles <- paste0(lazyBase, c(".rdx", ".rdb"))

    userObjNames <- ls(sim@.xData, all.names = FALSE)
    if (length(userObjNames)) {
      ## makeLazyLoadDB is internal to tools but stable; use getFromNamespace
      ## to avoid the R CMD check NOTE for ::: usage.
      ## Restrict to user objects: dot-prefixed internals (.mods, .modObjs, ...)
      ## stay in @.xData and ride along in saveRDS(). Without `variables`,
      ## makeLazyLoadDB defaults to all.names = TRUE and bundles each nested
      ## env's bindings into a single serialized blob via its envhook --
      ## .mods alone can exceed the 2 GB single-value lazyLoadDB limit.
      utils::getFromNamespace("makeLazyLoadDB", "tools")(
        sim@.xData, lazyBase, variables = userObjNames
      )
      rm(list = userObjNames, envir = sim@.xData)
    }

    if (ext == "rds") {
      saveRDS(sim, file = filename)
    } else {
      qs2::qs_save(sim, file = filename, nthreads = getOption("spades.qsThreads", 1))
    }

    if (isTRUE(files) && length(fns)) {
      fileToDelete <- filename
      otherFns <- c()
      if (isTRUE(outputs)) {
        os <- outputs(sim)
        if (NROW(os)) otherFns <- c(otherFns, os[os$saved %in% TRUE]$file)
      }
      if (isTRUE(inputs)) {
        ins <- inputs(sim)
        if (NROW(ins)) otherFns <- c(otherFns, ins[ins$loaded %in% TRUE, ]$file)
      }

      srcFiles <- mapply(mod = modules(sim), mp = modulePath(sim),
                         function(mod, mp) {
                           fls <- dir(file.path(mp, mod), recursive = TRUE, full.names = TRUE)
                           grep("^\\<data\\>", invert = TRUE, value = TRUE, fls)
                         })
      srcFilesRel <- makeRelative(srcFiles, projectPath)
      if (any(isAbsolutePath(srcFilesRel))) {
        guessProjPath <- fs::path_common(origPaths["modulePath"]) |> unique() |> dirname()
        srcFilesRel <- makeRelative(srcFiles, guessProjPath)
        tmpSrcFiles <- file.path(projectPath, srcFilesRel)
        linkOrCopy(srcFiles, tmpSrcFiles, verbose = verbose - 1)
        on.exit(unlink(tmpSrcFiles))
        srcFiles <- tmpSrcFiles
      }

      lazyDbExisting <- lazyDbFiles[file.exists(lazyDbFiles)]
      allFns <- c(fns, otherFns, srcFilesRel)
      if (!is.null(symlinks)) {
        for (p in names(symlinks)) {
          allFns <- gsub(origPaths[[p]], symlinks[[p]], allFns)
        }
      }
      allFns <- na.omit(allFns)

      relFns <- makeRelative(c(fileToDelete, lazyDbExisting, allFns), projectPath) |> unname()
      archiveWrite(filename, relFns, verbose)
      unlink(fileToDelete)
      unlink(lazyDbExisting)
    }

    messageVerbose("    ... saved!", verbose = verbose)
    return(invisible())
  }

  # filename <- gsub(tools::file_ext(filename), "qs2", filename)
  if (tolower(tools::file_ext(filename)) == "rds") {
    saveRDS(sim, file = filename)
  } else if (tolower(tools::file_ext(filename)) == "qs2") {
    filename <- gsub(tools::file_ext(filename), "qs2", filename)
    qs2::qs_save(sim, file = filename, nthreads = getOption("spades.qsThreads", 1))
  }

  if (isTRUE(files)) {
    srcFiles <- mapply(mod = modules(sim), mp = modulePath(sim),
                   function(mod, mp) {
                     files <- dir(file.path(mp, mod), recursive = TRUE, full.names = TRUE)
                     files <- grep("^\\<data\\>", invert = TRUE, value = TRUE, files)
                   })
    srcFilesRel <- makeRelative(srcFiles, projectPath)
    if (any(isAbsolutePath(srcFilesRel))) {
      ## means not inside the projectPath
      guessProjPath <- fs::path_common(origPaths["modulePath"]) |> unique() |> dirname()
      srcFilesRel <- makeRelative(srcFiles, guessProjPath)
      tmpSrcFiles <- file.path(projectPath, srcFilesRel)
      linkOrCopy(srcFiles, tmpSrcFiles, verbose = verbose - 1)
      on.exit(unlink(tmpSrcFiles))
      srcFiles <- tmpSrcFiles
    }

    if (length(fns)) {
      fileToDelete <- filename

      otherFns <- c()
      if (isTRUE(outputs)) {
        os <- outputs(sim)
        if (NROW(os)) {
          outputFNs <- os[os$saved %in% TRUE]$file
          otherFns <- c(otherFns, outputFNs)
        }
      }
      inputFNs <- NULL
      if (isTRUE(inputs)) {
        ins <- inputs(sim)
        if (NROW(ins)) {
          inputFNs <- ins[ins$loaded %in% TRUE, ]$file
          otherFns <- c(otherFns, inputFNs)
        }
      }

      allFns <- c(fns, otherFns, srcFilesRel)
      if (!is.null(symlinks)) {
        for (p in names(symlinks)) {
          allFns <- gsub(origPaths[[p]], symlinks[[p]], allFns)
        }
      }
      allFns <- na.omit(allFns)

      relFns <- makeRelative(c(fileToDelete, allFns), projectPath) |> unname()

      archiveWrite(filename, relFns, verbose)

      unlink(fileToDelete)
    }
  }
  messageVerbose("    ... saved!", verbose = verbose)

  return(invisible())
}

#' Zip a `simList` and various files
#'
#' `zipSimList` will save the `simList` and file-backed `Raster*` objects, plus,
#' optionally, files identified in `outputs(sim)` and `inputs(sim)`.
#' This uses `Copy` under the hood, to not affect the original `simList`.
#'
#' @inheritParams saveSimList
#' @param zipfile A character string indicating the filename for the zip file. Passed to `zip`.
#'
#' @export
#' @rdname deprecated
zipSimList <- function(sim, zipfile, ..., outputs = TRUE, inputs = TRUE, cache = FALSE) {
  .Deprecated("saveSimList")
  saveSimList(sim, filename = zipfile)
}

#' Load a saved `simList` and ancillary files
#'
#' Loading a `simList` from file can be problematic as there are non-standard
#' objects that must be rebuilt. See description in [saveSimList()] for details.
#'
#' @param filename Character giving the name of a saved simulation file.
#'   Currently, only file types `.qs2` or `.rds` are supported.
#' @param projectPath An optional path for the project within which the `simList`
#'   exists. This is used to identify relative paths for saving and loading the `simList`.
#' @param paths A list of character vectors for all the `simList` paths. When
#'   loading a `simList`, this will replace the paths of everything to
#'   these new paths. Experimental still.
#' @param otherFiles A character vector of (absolute) file names locating each of the
#'   existing file-backed `Raster*` files that are the real paths for the possibly
#'   incorrect paths in `Filenames(sim)` if the the `file` being read in is from
#'   a different computer, path, or drive. This could be the output from `unzipSimList`
#'   (which is calls `loadSimList` internally, passing the unzipped filenames)
#' @param tempPath A character string specifying the new base directory for the
#'   temporary paths maintained in a `simList`.
#' @inheritParams reproducible::Cache
#'
#' @return For [loadSimList()], a `simList` object.
#'         For [unzipSimList()], either a character vector of file names unzipped
#'         (if `load = FALSE`), or a `simList` object.
#'
#' @export
#' @rdname loadSimList
#' @seealso [saveSimList()], [zipSimList()]
#' @importFrom qs2 qs_read
#' @importFrom reproducible linkOrCopy remapFilenames updateFilenameSlots .unwrap
#' @importFrom tools file_ext
loadSimList <- function(filename, projectPath = getwd(), tempPath = tempdir(),
                        paths = NULL, otherFiles = "",
                        verbose = getOption("reproducible.verbose")) {
  checkSimListExts(filename)

  filename <- checkArchiveAlternative(filename)

  if (grepl(archiveExts, tolower(tools::file_ext(filename)))) {
    td <- tempdir2(sub = .rndstr())
    filename <- archiveExtract(filename, exdir = td)
    on.exit(unlink(td, recursive = TRUE), add = TRUE)

    baseNameNoExt <- tools::file_path_sans_ext(basename(filename[1]))
    lazyBaseName <- paste0(baseNameNoExt, "_xData")
    lazyDbBasenames <- paste0(lazyBaseName, c(".rdx", ".rdb"))
    isLazyDb <- basename(filename[-1]) %in% lazyDbBasenames

    filenameRel <- gsub(paste0(td, "/"), "", filename[-1][!isLazyDb])  ## TODO: WRONG!
    ## This will put the files to relative path of projectPath
    newFns <- file.path(projectPath, filenameRel)
    linkOrCopy(filename[-1][!isLazyDb], newFns, verbose = verbose - 1)

    ## Persist .rdb/.rdx beyond tempdir cleanup so lazy promises can resolve
    lazyBase <- file.path(tempPath, lazyBaseName)
    for (ex in c(".rdx", ".rdb")) {
      srcs <- filename[-1][isLazyDb & basename(filename[-1]) == paste0(lazyBaseName, ex)]
      if (length(srcs)) {
        dst <- paste0(lazyBase, ex)
        if (file.exists(dst)) unlink(dst)
        file.copy(srcs[[1L]], dst)
      }
    }
  } else {
    # filenameRel <- gsub(paste0(projectPath, "/"), "", filename) ## TODO: WRONG!
    filenameRel <- getRelative(filename, projectPath)
    lazyBase <- paste0(tools::file_path_sans_ext(filename[1]), "_xData")
  }

  if (tolower(tools::file_ext(filename[1])) == "rds") {
    tmpsim <- readRDS(filename[1])
  } else if (tolower(tools::file_ext(filename[1])) == "qs2") {
    tmpsim <- qs2::qs_read(filename[1], nthreads = getOption("spades.qsThreads", 1))
  }
  if (!is.null(paths)) {
    paths <- lapply(paths, normPath)
  } else {
    paths <- list()
  }

  ## TODO: figure out what is inserting 'NA' into some paths during saveSimList
  paths(tmpsim) <- paths(tmpsim) |>
    # sapply(function(pth) {
    #   if (fs::path_has_parent(pth, "NA")) {
    #     gsub("NA/", "./", pth) |> fs::path_norm() |> as.character()
    #   } else {
    #     pth
    #   }
    # }, simplify = FALSE) |>
    modifyList2(paths)

  paths(tmpsim) <- absolutizePaths(paths(tmpsim), projectPath, tempPath)

  ## remap all the file-backed objects. their paths in the objects will point
  ## to their old locations, but they are now at newFns, which is remapped to projectPath
  oldFns <- Filenames(tmpsim, returnList = TRUE)
  oldFns <- FilterRecursive(length, oldFns) ## handles nested lists (e.g. scfm objs, biomassModel)

  for (nam in names(oldFns)) {
    tags <- attr(tmpsim[[nam]], "tags")
    if (!is.null(tags)) {
      if (identical(projectPath, getwd())) {
        pths <- paths(tmpsim)
      } else {
        pths <- list(projectPath = projectPath)
      }

      newFiles <- remapFilenames(tags = tags, cachePath = NULL, paths = pths)

      if (is(tmpsim[[nam]], "list")) {
        # lists are weird; historicalClimateLayers was length 2 list; had 4 filenames, 2 repeated twice
        #   need to fix this
        newNames <- unique(newFiles$newName)
        for (elem in names(tmpsim[[nam]])) {
          fileHere <- tmpsim[[nam]][[elem]] # should only have 1 element's file(s)
          if (!is.character(fileHere) || !length(fileHere)) next # NULL'd by .wrapResiliently
          dirToFileHere <- dirname(fileHere)
          nParents <- attr(fileHere, "nParentDirs")
          1
          #if (nParents > 0) {
          for (nPar in rev(seq(nParents + 1))) {
            dirToFileHere <- dirname(dirToFileHere)
          }
          newNames1 <- fs::path_rel(newNames, dirToFileHere)
          thisFile <- fs::path_rel(fileHere, dirToFileHere)
          newNames1 <- newNames1[newNames1 %in% thisFile]
          newNamesHere <- file.path(dirToFileHere, newNames1)
          tmpsim[[nam]][[elem]][] <- unique(newNamesHere)
        }
      } else {
        tmpsim[[nam]][] <- newFiles$newName[]
      }
    }
  }

  ## Detect lazy load before .unwrap so we can clear .modObjs (which holds per-module
  ## copies of file-backed objects) from the shell simList. Those objects were NOT saved
  ## as part of the shell — their backing files may not exist — and .unwrap would fail on
  ## them. They are rebuilt by spades() on the next run, so it is safe to clear them here.
  isLazyLoad <- file.exists(paste0(lazyBase, ".rdx"))

  if (isLazyLoad) {
    modObjsEnv <- tmpsim@.xData[[".modObjs"]]
    if (is.environment(modObjsEnv)) {
      nms <- ls(modObjsEnv, all.names = TRUE)
      if (length(nms)) rm(list = nms, envir = modObjsEnv)
    } else if (!is.null(modObjsEnv)) {
      tmpsim@.xData[[".modObjs"]] <- NULL
    }
  }

  tmpsim <- .unwrapResiliently(tmpsim, paths(tmpsim))
  tmpsim <- .unwrap(tmpsim, cachePath = NULL, paths = paths(tmpsim))

  ## Work around for bug in qs that recovers data.tables as lists
  # tmpsim <- recoverDataTableFromQs(tmpsim)

  ## Deal with all the RasterBacked Files that will be wrong
  if (any(nchar(otherFiles) > 0)) {
    .dealWithRasterBackends(tmpsim) # no need to assign to sim b/c uses list2env
  }
  makeSimListActiveBindings(tmpsim)

  ## Lazy loading: if a sibling .rdx/.rdb pair exists, restore the user objects
  ## via lazyLoad() into a holding env, then bind delayedAssign wrappers on the
  ## simList so each access triggers the underlying lazy promise plus our
  ## remap/unwrap pass.
  if (isLazyLoad) {
    lazyEnv <- new.env(parent = emptyenv())
    tryCatch(lazyLoad(lazyBase, envir = lazyEnv),
             error = function(e) warning("Could not attach lazy DB '", lazyBase,
                                         "': ", conditionMessage(e), call. = FALSE))
    simPaths <- paths(tmpsim)
    for (nm in ls(lazyEnv, all.names = TRUE)) {
      local({
        .nm <- nm
        .lazyEnv     <- lazyEnv
        .projectPath <- projectPath
        .simPaths    <- simPaths
        delayedAssign(.nm, tryCatch({
          obj <- get(.nm, envir = .lazyEnv)
          obj <- .remapFileBackedObj(obj, .projectPath, .simPaths)
          .unwrap(obj, cachePath = NULL, paths = .simPaths)
        }, error = function(e) {
          warning("Could not load lazy object '", .nm, "': ", conditionMessage(e), call. = FALSE)
          NULL
        }), eval.env = environment(), assign.env = envir(tmpsim))
      })
    }
  }

  return(tmpsim)
}

#' `unzipSimList` will unzip a zipped `simList`
#'
#' `unzipSimList` is a convenience wrapper around `unzip` and `loadSimList` where
#' all the files are correctly identified and passed to
#' `loadSimList(..., otherFiles = xxx)`. See [zipSimList] for details.
#'
#' @details
#' If `cache` is used, it is likely that it should be trimmed before
#' zipping, to include only cache elements that are relevant.
#'
#' @param zipfile Filename of a zipped `simList`
#' @param load Logical. If `TRUE`, the default, then the `simList` will
#'   also be loaded into R.
#' @param ... passed to `unzip`
#'
#' @export
#' @rdname loadSimList
unzipSimList <- function(zipfile, load = TRUE, paths = getPaths(), ...) {
  .Deprecated("loadSimList")
  sim <- loadSimList(zipfile, ...)
  return(sim)
}

checkArchiveAlternative <- function(filename) {
  if (!file.exists(filename[1])) {
    baseN <- tools::file_path_sans_ext(basename(filename))
    possZips <- dir(dirname(filename), pattern = paste0(baseN, ".", archiveExts),
                    full.names = TRUE)
    if (length(possZips)) {
      filename <- possZips[1]
    }

  }
  filename
}

archiveExts <- "(tar$|tar\\.gz$|zip$|gz$)"

## TODO: is this still needed when using qs2??
#' @importFrom data.table as.data.table data.table rbindlist
recoverDataTableFromQs <- function(sim) {
  objectName <- ls(sim)
  names(objectName) <- objectName
  objectClassInSim <- lapply(objectName, function(x) is(get(x, envir = sim))[1])
  dt <- data.table(objectName, objectClassInSim)

  io <- inputObjects(sim)
  oo <- outputObjects(sim)
  if (is(io, "list")) io <- rbindlist(io, fill = TRUE)
  if (is(oo, "list")) oo <- rbindlist(oo, fill = TRUE)
  objs <- rbindlist(list(io, oo), fill = TRUE)
  objs <- unique(objs, by = "objectName")[, c("objectName", "objectClass")]

  objs <- objs[dt, on = "objectName"]
  objs <- objs[objectClass == "data.table" & objectClassInSim != "disk.frame"]
  objs <- objs[objectClass != objectClassInSim]
  if (NROW(objs)) {
    message("There is a bug in qs package that recovers data.table objects incorrectly when in a list")
    message("Converting all known data.table objects (according to metadata) from list to data.table")
    simEnv <- envir(sim)
    out <- lapply(objs$objectName, function(on) {
      tryCatch({
        assign(on, copy(as.data.table(sim[[on]])), envir = simEnv)
      }, error = function(e) warning(e))
    })
  }
  sim
}

.dealWithRasterBackends <- function(otherFiles, sim, paths) {
  pathsInOldSim <- paths(sim)
  sim@paths <- paths
  fnsSingle <- Filenames(sim, allowMultiple = FALSE)
  newFns <- Filenames(sim)

  fnsObj <- sim@.xData$._rasterFilenames
  origFns <- normPath(fnsObj$filenames)
  objNames <- fnsObj$topLevelObjs
  objNames <- setNames(objNames, objNames)

  newFns <- vapply(origFns, function(fn) {
    fnParts <- strsplit(fn, split = "\\/")[[1]]
    relParts <- vapply(fnParts, grepl, x = unlist(pathsInOldSim),
                       logical(length(pathsInOldSim))) # 5 paths components
    whRel <- which(apply(relParts, 2, sum) == 0)
    whAbs <- whRel[1] - 1
    whAbs <- which.max(apply(relParts, 1, sum))
    # use new paths as base for newFns
    newPath <- file.path(paths[[whAbs]], fnParts[whRel[1]], basename(fn))
  }, character(1))

  reworkedRas <- lapply(objNames, function(objName) {
    namedObj <- grep(objName, names(newFns), value = TRUE)
    newPaths <- dirname(newFns[namedObj])
    names(newPaths) <- names(newFns[namedObj])
    dups <- duplicated(newPaths)
    if (any(dups)) {
      newPaths <- newPaths[!dups]
    }

    dups2ndLayer <- duplicated(newPaths)
    if (any(dups2ndLayer)) {
      stop("Cannot unzip and rebuild lists with rasters with multiple different paths; ",
           "Please simplify the list of Rasters so they all share a same dirname(Filenames(ras))")
    }

    # These won't exist because they are the filenames from the old
    #   (possibly temporary following saveSimList) simList
    fns <- Filenames(sim[[objName]], allowMultiple = FALSE)

    # Now match them with the files that exist from unzipping
    currentFname <- unlist(lapply(fns, function(fn) {
      grep(basename(fn),
           otherFiles, value = TRUE)
    }))
    currentDir <- unique(dirname(currentFname))

    # First must update the filename slots so that they point to real files (in the exdir)
    sim[[objName]] <- updateFilenameSlots(sim[[objName]],
                                          newFilenames = currentDir)
    mess <- capture.output(type = "message", {
      sim[[objName]] <- (Copy(sim[[objName]], fileBackend = 1, filebackedDir = newPaths))
    })
    mess <- grep("Hardlinked version", mess, invert = TRUE)
    if (length(mess))
      lapply(mess, message)
    return(sim[[objName]])
  })

  list2env(reworkedRas, envir = envir(sim))
}

## Recursively clone every environment reachable from a simList so that
## subsequent rebindings inside saveSimList don't mutate the caller's state.
## Leaf values (SpatRasters, data.tables, lists, etc.) are kept by reference
## — saveSimList rebinds names in the cloned envs rather than mutating the
## internals of those leaves, and avoiding leaf duplication is the whole
## point of doing this instead of reproducible::Copy(sim) (which eagerly
## duplicates SpatRaster backing files).
.cloneSimEnvs <- function(sim) {
  sim@.xData    <- .cloneEnvDeep(sim@.xData)
  sim@.envir    <- sim@.xData
  sim@completed <- .cloneEnvDeep(sim@completed)
  sim
}

## Recursive environment cloner. For each binding: if it's an environment,
## recurse; otherwise pass it through. Preserves attributes.
.cloneEnvDeep <- function(env, .seen = NULL) {
  if (!is.environment(env)) return(env)
  if (is.null(.seen)) .seen <- new.env(parent = emptyenv())
  key <- format(env)
  if (exists(key, envir = .seen, inherits = FALSE)) {
    return(get(key, envir = .seen, inherits = FALSE))
  }
  out <- new.env(parent = parent.env(env))
  assign(key, out, envir = .seen)  # cycle guard before recursing
  for (nm in ls(env, all.names = TRUE)) {
    if (bindingIsActive(nm, env)) {
      ## Re-attach the same active binding (its backing function is what
      ## defines its behavior; copying the function is enough).
      makeActiveBinding(nm, activeBindingFunction(nm, env), out)
    } else {
      val <- get(nm, envir = env, inherits = FALSE)
      assign(nm, .cloneEnvDeep(val, .seen = .seen), envir = out)
    }
  }
  attributes(out) <- attributes(env)
  out
}

## Pre-wrap each file-backed object in sim@.xData individually so that one
## inaccessible backing file does not abort saveSimList. Failed objects are
## replaced with NULL and a warning is issued; the subsequent monolithic
## .wrap(sim, ...) then succeeds on the remaining objects.
.wrapResiliently <- function(sim) {
  nms <- ls(sim@.xData, all.names = FALSE)
  simPaths <- paths(sim)
  for (nm in nms) {
    obj <- sim@.xData[[nm]]
    fns <- tryCatch(Filenames(obj), error = function(e) character(0))
    if (length(fns) && any(nchar(fns) > 0L)) {
      sim@.xData[[nm]] <- tryCatch(
        .wrap(obj, cachePath = NULL, paths = simPaths),
        error = function(e) {
          warning("saveSimList: could not wrap '", nm,
                  "' (backing file inaccessible); saving as NULL.\n",
                  "  Error: ", conditionMessage(e), call. = FALSE)
          NULL
        }
      )
    }
  }
  sim
}

## Pre-unwrap each file-backed object in sim@.xData individually so that one
## inaccessible backing file does not abort loadSimList. Failed objects are
## replaced with NULL and a warning is issued; the subsequent monolithic
## .unwrap(sim, ...) then succeeds on the remaining objects. Mirror image of
## .wrapResiliently — load-time failures are independent of save-time
## failures (e.g. backing files may have been present at save time but
## missing on the machine doing the load).
.unwrapResiliently <- function(sim, simPaths) {
  nms <- ls(sim@.xData, all.names = FALSE)
  for (nm in nms) {
    obj <- sim@.xData[[nm]]
    if (is.null(attr(obj, "tags"))) next  # not wrapped
    fns <- tryCatch(Filenames(obj), error = function(e) character(0))
    if (length(fns) && any(nchar(fns) > 0L)) {
      sim@.xData[[nm]] <- tryCatch(
        .unwrap(obj, cachePath = NULL, paths = simPaths),
        error = function(e) {
          warning("loadSimList: could not unwrap '", nm,
                  "' (backing file inaccessible); loading as NULL.\n",
                  "  Error: ", conditionMessage(e), call. = FALSE)
          NULL
        }
      )
    }
  }
  sim
}

## Remap file paths in a single wrapped object (mirrors the per-object logic in
## loadSimList's remap loop, extracted so lazy promises can reuse it).
.remapFileBackedObj <- function(obj, projectPath, simPaths) {
  tags <- attr(obj, "tags")
  if (is.null(tags)) return(obj)
  ## Only remap objects that are truly file-backed (mirrors the Filenames filter in
  ## the non-lazy remap loop — wrapped non-file objects have tags but no filenames).
  fns <- tryCatch(Filenames(obj), error = function(e) character(0))
  if (!length(fns) || all(nchar(fns) == 0L)) return(obj)
  pths <- if (identical(projectPath, getwd())) simPaths else list(projectPath = projectPath)
  newFiles <- remapFilenames(tags = tags, cachePath = NULL, paths = pths)
  if (is(obj, "list")) {
    newNames <- unique(newFiles$newName)
    for (elem in names(obj)) {
      fileHere      <- obj[[elem]]
      if (!is.character(fileHere) || !length(fileHere)) next # NULL'd by .wrapResiliently
      dirToFileHere <- dirname(fileHere)
      nParents      <- attr(fileHere, "nParentDirs")
      for (nPar in rev(seq(nParents + 1))) {
        dirToFileHere <- dirname(dirToFileHere)
      }
      newNames1    <- fs::path_rel(newNames, dirToFileHere)
      thisFile     <- fs::path_rel(fileHere, dirToFileHere)
      newNames1    <- newNames1[newNames1 %in% thisFile]
      newNamesHere <- file.path(dirToFileHere, newNames1)
      obj[[elem]][] <- unique(newNamesHere)
    }
  } else {
    obj[] <- newFiles$newName[]
  }
  obj
}

checkSimListExts <- function(filename) {
  stopifnot(grepl(paste0("(qs2$|rds$)|", archiveExts), tolower(tools::file_ext(filename))))
}

warnDeprecFileBacked <- function(arg) {
  switch(
    tolower(arg),
    filebackeddir = paste0(
      "filebackedDir is deprecated; use projectPath and optionally ",
      "set individual path arguments, such as modulePath."
    ),
    filebackend = paste0(
      "fileBackend argument is deprecated; file-backed objects are ",
      "now maintained; for memory only objects, convert them to RAM objects ",
      "prior to saveSimList"
    ),
    stop("No deprecation warning with that arg: ", arg)
  )
}

archiveExtract <- function(archiveName, exdir) {
  if (requireNamespace("archive") && !isWindows()) {
    archiveName <- archiveConvertFileExt(archiveName, "tar.gz")
    filename <- archive::archive_extract(archiveName)
  } else {
    filename <- unzip(archiveName, exdir = exdir)
  }
  filename
}

archiveWrite <- function(archiveName, relFns, verbose) {
  relFns <- unname(relFns)

  if (requireNamespace("archive") && !isWindows()) {
    archiveName <- archiveConvertFileExt(archiveName, "tar.gz")
    # archiveName <- gsub(tools::file_ext(archiveName), "tar.gz", archiveName)
    compLev <- getOption("spades.compressionLevel", 1)
    archive::archive_write_files(
      archiveName,
      relFns,
      options = paste0("compression-level=", compLev)
    )
    # archive::archive_write_files(archiveName, files = relFns)
  } else {
    archiveName <- archiveConvertFileExt(archiveName, "zip")
    # archiveName <- gsub(tools::file_ext(archiveName), "zip", archiveName)
    ## the qs2 file doesn't deflate at all
    extras <- list("--compression-method store", NULL)
    if (verbose <= 0) {
      extras <- lapply(extras, function(ex) c(ex, "--quiet"))
    }
    zip(archiveName, files = relFns[1], extras = extras[[1]])
    zip(archiveName, files = relFns[-1], extras = extras[[2]])
  }
}

archiveConvertFileExt <- function(filename, convertTo = "tar.gz") {
  if (!(endsWith(filename, "tar.gz") && identical(convertTo, "tar.gz"))) {
    filename <- gsub(tools::file_ext(filename), convertTo, filename)
  }
  filename
}

#' @importFrom fs path_common path_norm
#' @importFrom reproducible getRelative makeRelative
relativizePaths <- function(paths, projectPath = NULL) {
  # p <- normPath(paths)
  p <- sapply(paths, fs::path_norm, USE.NAMES = TRUE)
  if (is.null(projectPath)) {
    projectPath <- fs::path_common(p[["modulePath"]]) |> unique() |> dirname()
  }
  p[corePaths] <- getRelative(p[corePaths], projectPath)
  p[tmpPaths] <- makeRelative(p[tmpPaths], p[["scratchPath"]])

  ## TODO: recombine paths, e.g. modulePath1, modulePath2 into modulePath
  p
}

#' @importFrom fs path_abs
absolutizePaths <- function(paths, projectPath, tempdir = tempdir()) {
  p <- paths
  p[corePaths] <- sapply(paths[corePaths], fs::path_abs, start = projectPath)
  p[tmpPaths] <- sapply(paths[tmpPaths], fs::path_abs, start = tempdir)
  lapply(p, normPath)
}
