#' @title Resampling temporal resolution of dendrometer and climate data
#'
#' @description
#' This function changes the temporal resolution of dendrometer and climate
#' time series. It supports both aggregation to coarser resolutions and
#' interpolation to finer resolutions. The target resolution can be defined in
#' minutes, hours, days, weeks, or month-end frequency. Aggregation can be
#' performed using \code{mean}, \code{max}, \code{min}, \code{sum}, or
#' \code{count} (number of non-missing values). When a finer temporal
#' resolution than the original data is requested, the function can interpolate
#' the series to the new time step using linear interpolation.
#'
#' @param df A dataframe with the first column containing date-time values in
#'   the format \code{yyyy-mm-dd HH:MM:SS}.
#'
#' @param by A character string defining the target temporal resolution.
#'   Supported values are:
#'   \itemize{
#'     \item \code{"M"} for minutes
#'     \item \code{"H"} for hours
#'     \item \code{"D"} for days
#'     \item \code{"W"} for weeks
#'     \item \code{"ME"} for month-end
#'     \item values such as \code{"10M"}, \code{"30M"}, \code{"5H"},
#'     \code{"2D"}, or \code{"3W"} for custom multiples
#'   }
#'
#' @param value A character string defining the aggregation function.
#'   Supported values are \code{"mean"}, \code{"max"}, \code{"min"},
#'   \code{"sum"}, and \code{"count"}.
#'   The \code{"count"} option returns the number of non-\code{NA} values
#'   within each aggregation interval. It is only valid when
#'   \code{method = "aggregate"}.
#'
#' @param method A character string defining how resampling should be performed.
#'   Options are:
#'   \itemize{
#'     \item \code{"auto"}: automatically aggregates when the requested
#'     resolution is coarser than the input data, and interpolates when the
#'     requested resolution is finer
#'     \item \code{"aggregate"}: always aggregates values within each target
#'     interval
#'     \item \code{"interpolate"}: always interpolates to the requested regular
#'     time grid
#'   }
#'
#' @return A dataframe with resampled data. The first column contains the new
#'   time stamps and the remaining columns contain the resampled variables.
#'
#' @examples
#' \donttest{
#' library(dendRoAnalyst)
#' data(nepa17)
#'
#' # Monthly resampling using maximum values
#' resample_ME <- dendro.resample(df = gf_nepa17, by = "ME", value = "max")
#' head(resample_ME, 10)
#'
#' # Daily resampling using mean values
#' resample_D <- dendro.resample(df = gf_nepa17, by = "D", value = "mean")
#'
#' # Count non-missing values per day
#' resample_count <- dendro.resample(df = gf_nepa17, by = "D", value = "count",
#'                                   method = "aggregate")
#'
#' # Interpolate to 30-minute resolution
#' resample_30M <- dendro.resample(df = gf_nepa17, by = "30M", value = "mean",
#'                                 method = "auto")
#' }
#'
#' @importFrom stats approx median na.exclude na.omit sd
#' @importFrom lubridate ymd_hms ymd floor_date minutes seconds minute second
#' @importFrom dplyr mutate group_by summarise ungroup %>% rename across select where case_when everything all_of
#' @importFrom tibble as_tibble tibble
#' @importFrom ggplot2 ggplot geom_area geom_rect geom_text aes theme_minimal labs geom_line geom_point facet_wrap theme element_text
#' @importFrom tidyr pivot_longer
#'
#' @export

dendro.resample <- function(df, by, value = "mean",
                            method = c("auto", "aggregate", "interpolate")) {
  TIME <-period <- NULL
  method <- match.arg(method)

  if (!requireNamespace("dplyr", quietly = TRUE) ||
      !requireNamespace("tibble", quietly = TRUE) ||
      !requireNamespace("lubridate", quietly = TRUE)) {
    stop("Packages 'dplyr', 'tibble', and 'lubridate' are required.")
  }

  # -----------------------------
  # Parse target frequency
  # M = minutes, H = hours, D = days, W = weeks, ME = month-end
  # Examples: M, 10M, 30M, H, 5H, D, 2D, W, 3W, ME
  # -----------------------------
  parse_by <- function(by) {
    by <- toupper(trimws(by))

    if (by == "ME") {
      return(list(
        unit = "month",
        n = 1L,
        label = "ME",
        seconds = NA_real_
      ))
    }

    m <- regexec("^([0-9]*)(M|H|D|W)$", by)
    parts <- regmatches(by, m)[[1]]

    if (length(parts) != 3) {
      stop("Invalid 'by'. Use 'M', 'H', 'D', 'W', 'ME', or values like '10M', '30M', '5H', '2D', '3W'.")
    }

    n <- ifelse(parts[2] == "", 1L, as.integer(parts[2]))
    unit_code <- parts[3]

    unit <- switch(
      unit_code,
      "M" = "minute",
      "H" = "hour",
      "D" = "day",
      "W" = "week"
    )

    seconds <- switch(
      unit_code,
      "M" = n * 60,
      "H" = n * 3600,
      "D" = n * 86400,
      "W" = n * 7 * 86400
    )

    list(
      unit = unit,
      n = n,
      label = by,
      seconds = seconds
    )
  }

  # -----------------------------
  # Infer source temporal resolution
  # -----------------------------
  infer_resolution_seconds <- function(time_vec) {
    time_vec <- sort(time_vec)
    d <- diff(time_vec)
    d_sec <- as.numeric(d, units = "secs")
    d_sec <- d_sec[is.finite(d_sec) & d_sec > 0]

    if (length(d_sec) == 0) return(NA_real_)
    stats::median(d_sec)
  }

  # -----------------------------
  # Floor timestamps to custom bins
  # -----------------------------
  floor_to_custom <- function(x, step_seconds) {
    tzx <- attr(x, "tzone")
    secs <- as.numeric(x)
    floored <- floor(secs / step_seconds) * step_seconds
    as.POSIXct(floored, origin = "1970-01-01", tz = tzx)
  }

  # -----------------------------
  # Build interpolation grid using numeric seconds
  # -----------------------------
  make_interp_grid <- function(time_vec, target_info) {
    if (is.na(target_info$seconds)) {
      stop("Interpolation is not supported for 'ME'. Use method = 'aggregate'.")
    }

    tzx <- attr(time_vec, "tzone")
    x_min <- min(as.numeric(time_vec), na.rm = TRUE)
    x_max <- max(as.numeric(time_vec), na.rm = TRUE)

    xout_num <- seq(from = x_min, to = x_max, by = target_info$seconds)
    as.POSIXct(xout_num, origin = "1970-01-01", tz = tzx)
  }

  # -----------------------------
  # Interpolate numeric columns
  # -----------------------------
  interpolate_to_grid <- function(df, target_info) {
    xout <- make_interp_grid(df$TIME, target_info)
    out <- tibble::tibble(TIME = xout)

    value_cols <- names(df)[vapply(df, is.numeric, logical(1))]
    value_cols <- setdiff(value_cols, "TIME")

    for (col in value_cols) {
      y <- df[[col]]
      good <- !is.na(df$TIME) & !is.na(y)

      if (sum(good) < 2) {
        out[[col]] <- NA_real_
      } else {
        interp <- stats::approx(
          x = as.numeric(df$TIME[good]),
          y = y[good],
          xout = as.numeric(xout),
          method = "linear",
          rule = 1
        )
        out[[col]] <- interp$y
      }
    }

    out
  }

  # -----------------------------
  # Aggregate helper
  # -----------------------------
  aggregate_data <- function(df, target_info, value, by_original) {
    allowed_funs <- c("mean", "max", "min", "sum", "count")
    if (!value %in% allowed_funs) {
      stop("value must be one of: 'mean', 'max', 'min', 'sum', 'count'")
    }

    summarization_fun <- switch(
      value,
      mean  = function(x) if (all(is.na(x))) NA_real_ else mean(x, na.rm = TRUE),
      max   = function(x) if (all(is.na(x))) NA_real_ else max(x, na.rm = TRUE),
      min   = function(x) if (all(is.na(x))) NA_real_ else min(x, na.rm = TRUE),
      sum   = function(x) if (all(is.na(x))) NA_real_ else sum(x, na.rm = TRUE),
      count = function(x) sum(!is.na(x))
    )

    value_cols <- names(df)[vapply(df, is.numeric, logical(1))]
    value_cols <- setdiff(value_cols, "TIME")

    if (length(value_cols) == 0) {
      stop("No numeric columns found to resample.")
    }

    if (target_info$label == "ME") {
      res <- df %>%
        dplyr::mutate(period = lubridate::ceiling_date(TIME, "month") - lubridate::days(1)) %>%
        dplyr::group_by(period) %>%
        dplyr::summarise(
          dplyr::across(dplyr::all_of(value_cols), summarization_fun),
          .groups = "drop"
        ) %>%
        dplyr::rename(TIME = period)

      return(res)
    }

    period_col <- floor_to_custom(df$TIME, step_seconds = target_info$seconds)

    res <- df %>%
      dplyr::mutate(period = period_col) %>%
      dplyr::group_by(period) %>%
      dplyr::summarise(
        dplyr::across(dplyr::all_of(value_cols), summarization_fun),
        .groups = "drop"
      ) %>%
      dplyr::rename(TIME = period)

    if (toupper(trimws(by_original)) %in% c("D", "1D")) {
      res$TIME <- as.Date(res$TIME)
    }

    res
  }

  # -----------------------------
  # Convert first column to datetime
  # -----------------------------
  if (!inherits(df[[1]], "POSIXt") && !inherits(df[[1]], "Date")) {
    parsed_time <- suppressWarnings(
      lubridate::parse_date_time(
        df[[1]],
        orders = c(
          "ymd HMS", "ymd HM", "ymd",
          "dmy HMS", "dmy HM", "dmy",
          "mdy HMS", "mdy HM", "mdy"
        ),
        quiet = TRUE
      )
    )
    df[[1]] <- parsed_time
  }

  if (inherits(df[[1]], "Date")) {
    df[[1]] <- as.POSIXct(df[[1]])
  }

  df <- tibble::as_tibble(df) %>%
    dplyr::rename(TIME = 1) %>%
    dplyr::arrange(TIME)

  if (any(is.na(df$TIME))) {
    stop("Some timestamps could not be parsed.")
  }

  target_info <- parse_by(by)
  source_sec <- infer_resolution_seconds(df$TIME)

  # -----------------------------
  # Decide operation
  # -----------------------------
  op <- method

  if (method == "auto") {
    if (target_info$label == "ME") {
      op <- "aggregate"
    } else if (is.na(source_sec)) {
      op <- "aggregate"
    } else if (target_info$seconds < source_sec) {
      op <- "interpolate"
    } else {
      op <- "aggregate"
    }
  }

  # -----------------------------
  # Run operation
  # -----------------------------
  if (op == "interpolate") {
    if (value == "count") {
      stop("'count' is not valid with method = 'interpolate'.")
    }
    return(interpolate_to_grid(df, target_info))
  }

  if (op == "aggregate") {
    return(aggregate_data(df, target_info, value, by))
  }

  stop("Unknown method.")
}
