#' Scale Environmental Rasters Using Species Occurrence Data
#'
#' Preprocessing function that standardizes raster files using precomputed mean
#' and standard deviation values from species occurrence data. Supports both
#' temporally static and dynamic rasters.
#'
#' @usage
#' scale_rasters(input_dir, output_dir, scaling_params_file,
#'               variable_patterns, time_cols = NULL, output_suffix = "_Scaled",
#'               overwrite = FALSE, verbose = TRUE)
#'
#' @param input_dir Character. Directory containing aligned input .tif raster files,
#' typically the output of \code{\link{raster_align}}.
#' @param output_dir Character. Directory where scaled rasters will be saved.
#' @param scaling_params_file Character. Path to CSV file with columns:
#'   variable, mean, sd. Typically generated by
#'   \code{\link{temporally_explicit_extraction}}.
#' @param variable_patterns Named character vector mapping clean variable names
#'   to raster filename patterns. For time-varying variables include the time
#'   placeholder in the pattern (e.g. \code{"forest_cover" = "forest_cover_YEAR"});
#'   for static variables omit it (e.g. \code{"elevation" = "elevation"}). Time
#'   placeholders must match entries in \code{time_cols}.
#' @param time_cols Character vector of time placeholders for dynamic variables.
#'   Default is \code{NULL}.
#' @param output_suffix Character. Suffix to append to output raster filenames.
#'   Default is \code{"_Scaled"}.
#' @param overwrite Logical. If \code{TRUE}, overwrites existing output files.
#'   If \code{FALSE} (default), existing files are skipped.
#' @param verbose Logical. If \code{TRUE} (default), prints progress
#'   messages during processing. Includes per-variable scaling progress.
#'
#' @return Invisibly returns a list containing:
#' \itemize{
#'   \item \code{n_scaled}: Integer. Total number of variables successfully
#'     scaled (static plus dynamic).
#'   \item \code{output_dir}: Character. Path to the directory containing scaled
#'     rasters.
#' }
#'
#' @details
#' Applies scaling to rasters via z-score transformation: (value - mean) / sd
#' as is calculated during \code{\link{temporally_explicit_extraction}}. Scaled
#' rasters are written to output_dir.
#'
#' Scaled rasters should use the same scaling parameters derived from species
#' occurrence data to ensure consistent standardization between training data
#' and prediction layers.
#'
#' @seealso
#' Preprocessing: \code{\link{temporally_explicit_extraction}},
#'   \code{\link{raster_align}}
#'
#' @examples
#' aln_dir     <- system.file("extdata/rasters_aligned",
#'                            package = "TemporalModelR")
#'
#' params_file <- system.file(
#'   "extdata/points/extracted_seasonal_Scaling_Parameters.csv",
#'   package = "TemporalModelR"
#' )
#'
#' out_dir <- file.path(tempdir(), "scaled")
#'
#' scale_rasters(
#'   input_dir           = aln_dir,
#'   output_dir          = out_dir,
#'   scaling_params_file = params_file,
#'   variable_patterns   = c(
#'     "elevation"    = "elevation",
#'     "forest_cover" = "forest_cover_YEAR",
#'     "prseas"       = "prseas_YEAR_SEASON"
#'   ),
#'   time_cols           = c("year", "season"),
#'   overwrite           = TRUE,
#'   verbose             = FALSE
#' )

#' @export
#' @importFrom terra rast writeRaster
#' @importFrom tools file_path_sans_ext file_ext
#' @importFrom utils read.csv
#' @importFrom stats setNames
scale_rasters <- function(input_dir,
                          output_dir,
                          scaling_params_file,
                          variable_patterns,
                          time_cols     = NULL,
                          output_suffix = "_Scaled",
                          overwrite     = FALSE,
                          verbose       = TRUE) {

  .validate_variable_patterns(variable_patterns)

  if (!dir.exists(input_dir)) {
    stop(paste0("ERROR: Input directory does not exist: ", input_dir))
  }

  if (!file.exists(scaling_params_file)) {
    stop(paste0("ERROR: Scaling parameters file does not exist: ", scaling_params_file))
  }

  dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

  scaling_params <- utils::read.csv(scaling_params_file, stringsAsFactors = FALSE)
  if (verbose) message(paste("Loaded scaling parameters for", nrow(scaling_params), "variables"))

  missing_params <- setdiff(names(variable_patterns), scaling_params$variable)
  if (length(missing_params) > 0) {
    warning(paste0("The following variables lack scaling parameters: ",
                   paste(missing_params, collapse = ", ")))
  }

  all_files <- list.files(path = input_dir, pattern = "\\.tif$",
                          recursive = TRUE, full.names = TRUE)
  if (verbose) message(paste("Found", length(all_files), "raster files"))

  if (length(all_files) == 0) {
    stop(paste0("ERROR: No .tif files found in input directory: ", input_dir))
  }

  if (overwrite) {
    if (verbose) message("Overwrite mode: ON - Will process and overwrite all files")
  } else {
    if (verbose) message("Overwrite mode: OFF - Will skip files that already exist")
  }

  var_classes  <- .classify_variable_patterns(variable_patterns, time_cols)
  dynamic_vars <- var_classes$dynamic
  static_vars  <- var_classes$static

  if (verbose) message(paste("Dynamic variables:",
                             if (length(dynamic_vars) > 0) paste(dynamic_vars, collapse = ", ") else "none"))
  if (verbose) message(paste("Static variables:",
                             if (length(static_vars) > 0) paste(static_vars, collapse = ", ") else "none"))

  static_vars_scaled  <- 0
  static_vars_skipped <- 0
  if (length(static_vars) > 0) {
    if (verbose) message("Processing static variables...")
    dummy_time_values <- as.data.frame(stats::setNames(
      lapply(time_cols, function(tc) NA_character_), time_cols
    ), stringsAsFactors = FALSE)
    for (var_name in static_vars) {
      if (verbose) message(paste("Scaling static variable:", var_name))
      var_params <- scaling_params[scaling_params$variable == var_name, ]
      if (nrow(var_params) == 0) {
        if (verbose) message(paste0("  Skipping ", var_name, " - no scaling parameters found"))
        next
      }
      var_mean <- var_params$mean
      var_sd   <- var_params$sd
      resolved <- .resolve_raster_paths(variable_patterns, character(0), var_name,
                                        time_cols, dummy_time_values, input_dir)
      if (is.null(resolved)) {
        warning(paste0("No files found for static variable ", var_name))
        next
      }
      file_path   <- resolved[1]
      output_file <- file.path(output_dir, paste0(var_name, output_suffix, ".tif"))
      if (file.exists(output_file) && !overwrite) {
        if (verbose) message("  Skipped (already exists)")
        static_vars_skipped <- static_vars_skipped + 1
      } else {
        raster_layer  <- terra::rast(file_path)
        raster_scaled <- (raster_layer - var_mean) / var_sd
        names(raster_scaled) <- tools::file_path_sans_ext(basename(output_file))
        terra::writeRaster(raster_scaled, output_file, overwrite = overwrite)
        static_vars_scaled <- static_vars_scaled + 1
      }
    }
  }

  dynamic_vars_scaled <- 0
  if (length(dynamic_vars) > 0) {
    if (verbose) message("Processing dynamic variables...")

    all_time_combos <- list()

    for (var_name in dynamic_vars) {
      search_pattern <- variable_patterns[var_name]
      relevant_time_cols <- time_cols[vapply(time_cols, function(tc) {
        grepl(tc, search_pattern, ignore.case = TRUE)
      }, logical(1))]
      if (length(relevant_time_cols) == 0) next

      pattern_parts  <- strsplit(search_pattern, "_")[[1]]
      tc_positions   <- vapply(relevant_time_cols, function(tc) {
        pos <- which(toupper(pattern_parts) == toupper(tc))
        if (length(pos) > 0) pos[1] else NA_integer_
      }, integer(1))

      candidate_pattern <- variable_patterns[var_name]
      for (tc in relevant_time_cols) {
        candidate_pattern <- gsub(tc, "[^_]+", candidate_pattern,
                                  ignore.case = TRUE)
      }
      candidate_pattern <- paste0("^", candidate_pattern, "(?=[^0-9]|$)")
      candidate_files <- all_files[vapply(all_files, function(f) {
        grepl(candidate_pattern, basename(f),
              ignore.case = TRUE, perl = TRUE)
      }, logical(1))]

      for (file_path in candidate_files) {
        filename_parts <- strsplit(tools::file_path_sans_ext(basename(file_path)), "_")[[1]]
        time_vals      <- stats::setNames(
          lapply(seq_along(relevant_time_cols), function(i) {
            pos <- tc_positions[i]
            if (!is.na(pos) && pos <= length(filename_parts)) filename_parts[pos] else NA_character_
          }),
          relevant_time_cols
        )
        if (any(is.na(unlist(time_vals)))) next
        ### Build full time_values row with all time_cols (NAs for cols not in this pattern)
        full_row <- stats::setNames(
          lapply(time_cols, function(tc) {
            if (tc %in% relevant_time_cols) time_vals[[tc]] else NA_character_
          }),
          time_cols
        )
        combo_key <- paste(vapply(relevant_time_cols, function(tc) time_vals[[tc]], character(1)), collapse = "_")
        all_time_combos[[combo_key]] <- as.data.frame(full_row, stringsAsFactors = FALSE)
      }
    }

    if (length(all_time_combos) == 0) {
      warning("No valid time information found in filenames for dynamic variables.")
    } else {
      if (verbose) message(paste("  Found", length(all_time_combos), "unique time periods"))

      for (var_name in dynamic_vars) {
        if (verbose) message(paste("Scaling dynamic variable:", var_name))
        var_params <- scaling_params[scaling_params$variable == var_name, ]
        if (nrow(var_params) == 0) {
          if (verbose) message(paste0("  Skipping ", var_name, " - no scaling parameters found"))
          next
        }
        var_mean <- var_params$mean
        var_sd   <- var_params$sd

        search_pattern <- variable_patterns[var_name]
        relevant_time_cols <- time_cols[vapply(time_cols, function(tc) {
          grepl(tc, search_pattern, ignore.case = TRUE)
        }, logical(1))]

        processed_count <- 0
        skipped_count   <- 0

        for (combo_key in names(all_time_combos)) {
          time_values <- all_time_combos[[combo_key]]

          if (any(is.na(unlist(time_values[relevant_time_cols])))) next

          resolved <- .resolve_raster_paths(variable_patterns, var_name, character(0),
                                            time_cols, time_values, input_dir)
          if (is.null(resolved)) next

          file_path   <- resolved[1]
          time_string <- paste(vapply(relevant_time_cols, function(tc) as.character(time_values[[tc]]), character(1)), collapse = "_")
          output_file <- file.path(output_dir, paste0(var_name, "_", time_string, output_suffix, ".tif"))

          if (file.exists(output_file) && !overwrite) {
            skipped_count <- skipped_count + 1
            next
          }
          raster_layer  <- terra::rast(file_path)
          raster_scaled <- (raster_layer - var_mean) / var_sd
          names(raster_scaled) <- tools::file_path_sans_ext(basename(output_file))
          terra::writeRaster(raster_scaled, output_file, overwrite = overwrite)
          processed_count <- processed_count + 1
        }

        if (processed_count > 0) dynamic_vars_scaled <- dynamic_vars_scaled + 1
        if (verbose) message(paste("  Processed:", processed_count, "| Skipped:", skipped_count))
      }
    }
  }

  total_vars_scaled  <- static_vars_scaled + dynamic_vars_scaled
  total_vars_skipped <- static_vars_skipped

  if (total_vars_scaled == 0 && total_vars_skipped == 0) {
    stop(paste0(
      "ERROR: No raster files could be matched and scaled for any variables. ",
      "Check that 'variable_patterns' values match your raster filenames and that ",
      "'time_cols' placeholders correspond to the time values present in those filenames. ",
      "See ?scale_rasters for details."
    ))
  }

  if (verbose) message("All scaling complete.")
  if (verbose) message(paste("  Static:", static_vars_scaled, "new |",
                             static_vars_skipped, "skipped | Dynamic:", dynamic_vars_scaled, "variables"))

  invisible(list(
    n_scaled   = total_vars_scaled,
    output_dir = output_dir
  ))
}
