#' @title Apply the Zero-Growth (ZG) approach to segment dendrometer data into TWD/GRO phases
#'
#' @description
#' Implements the Zero-Growth approach (Zweifel et al., 2016) on a single
#' dendrometer series to (i) classify each timestamp into \strong{tree water deficit}
#' (TWD; reversible shrinkage/expansion) or \strong{growth} (GRO; irreversible
#' expansion), (ii) summarize each contiguous phase (start/end, duration,
#' magnitude, rates, TWD statistics), and (iii) optionally smooth the raw
#' series prior to phase assignment to reduce spurious phase flips.
#'
#' The growth line is computed as the running cumulative maximum of the
#' (raw or smoothed) dendrometer series. TWD is defined as the vertical
#' distance between the growth line and the observed measurement.
#'
#' @details
#' \strong{Phase assignment.}
#' Let \eqn{y_t} be the dendrometer measurement (raw or smoothed). The growth
#' line is \eqn{g_t=\max_{s\le t}(y_s)}. Points with \eqn{y_t = g_t} are labeled
#' \strong{GRO} (phase = 2). Points with \eqn{y_t < g_t} are labeled \strong{TWD}
#' (phase = 1). Contiguous runs of identical labels are summarized into
#' individual phases.
#'
#' \strong{Summaries per phase (ZG_cycle).}
#' For each phase, the function returns:
#' \itemize{
#'   \item \code{Phases} (1 = TWD, 2 = GRO)
#'   \item \code{Start}, \code{End} (\code{POSIXct})
#'   \item \code{Duration_h} (hours)
#'   \item \code{Magnitude} (mm): for GRO only, computed from the \emph{growth line}
#'         as \eqn{\Delta g = g_{\mathrm{end}}-g_{\mathrm{start}}}; \code{NA} for TWD
#'   \item \code{rate} (\eqn{\mu}m/h): \code{Magnitude * 1000 / Duration_h} (GRO only)
#'   \item \code{max.twd} (mm), \code{Max.twd.time} (\code{POSIXct}): peak TWD and its time (TWD only)
#'   \item \code{Avg.twd} (mm), \code{STD.twd} (mm): mean and SD of TWD within the phase (TWD only)
#'   \item \code{AUC.load} (mm*h): area under the TWD curve from phase start to \code{Max.twd.time} (TWD only)
#'   \item \code{AUC.total} (mm*h): area under the full TWD curve across the phase (TWD only)
#'   \item \code{DOY}: day-of-year of the phase start
#'   \item \code{ABr.value}: \emph{Absolut Baumreaktion (ABr)} for TWD phases, defined here as
#'         \deqn{\mathrm{ABr} = \max(\mathrm{TWD}) \times \left(\frac{AUC_{load}}{\max(\mathrm{TWD})}\right)^{\beta}}
#'         returned as \code{NA} for GRO phases. This metric anchors severity on the event peak while
#'         allowing the loading duration to contribute with exponent \code{beta}.
#' }
#'
#' \strong{Point-level output (ZG_phase).}
#' The second element returns the input time series augmented with:
#' \itemize{
#'   \item \code{Phases} (1/2), \code{TWD} (= \code{GRO - dm}), and \code{GRO} (growth line).
#' }
#'
#' \strong{Smoothing (optional).}
#' If \code{smoothing} is provided (hours), the function uses
#' \code{smooth_dm(..., method = "median_mean", window_hours = smoothing)} to create a
#' smoothed series used \emph{only} for phase assignment; all magnitudes and TWD
#' statistics are still computed against the \emph{original} \code{dm}. This reduces
#' short-lived phase flips due to noise. Valid range is 1–24 hours.
#'
#' \strong{Temporal resolution.}
#' The function auto-detects the median sampling interval (in minutes) to
#' parameterize smoothing. If multiple distinct intervals are detected, a
#' warning is emitted (results may be degraded if resolution is inconsistent).
#'
#' \strong{Notes & caveats.}
#' \itemize{
#'   \item Negative \code{Magnitude} values in GRO phases indicate residual noise or
#'         insufficient smoothing; a warning is issued.
#'   \item Very short phases may occur around local extrema; consider smoothing
#'         or post-filtering minimal run lengths upstream if needed.
#'   \item Ensure timestamps are regular (or nearly regular) for best results.
#' }
#'
#' @references
#' Zweifel R, Haeni M, Buchmann N, Eugster W (2016) Are trees able to grow in periods of stem shrinkage?
#' \emph{New Phytologist}, 211:839–849. \doi{10.1111/nph.13995}
#'
#' @param df A data frame with the \strong{first column} containing date-time
#'   (character, \code{POSIXct}, or \code{Date} convertible to \code{POSIXct} using
#'   \code{lubridate::ymd_hms}) and the \strong{subsequent columns} containing one or
#'   more dendrometer series (mm) at constant temporal resolution.
#' @param TreeNum Integer index of the dendrometer series column to analyze.
#'   For example, \code{TreeNum = 1} refers to \code{df[[2]]} (the first dm column after
#'   the time column).
#' @param smoothing \code{NULL} (default) for no smoothing, or a numeric value
#'   between 1 and 24 specifying the smoothing window in \strong{hours} used by
#'   \code{smooth_dm(method = "median_mean")} for phase labeling.
#' @param beta Numeric scalar giving the exponent applied to the effective loading
#'   duration in the ABr formula. Defaults to \code{0.1}. Set \code{beta = 0} to
#'   make ABr depend only on \code{max.twd}.
#'
#' @return
#' A named list of class \code{"ZG_output"} with two tibbles:
#' \itemize{
#'   \item \code{ZG_cycle}: one row per contiguous phase with columns
#'         \code{Phases}, \code{Start}, \code{End}, \code{Duration_h}, \code{Magnitude}, \code{rate},
#'         \code{max.twd}, \code{Max.twd.time}, \code{AUC.load}, \code{AUC.total}, \code{ABr.value},
#'         \code{Avg.twd}, \code{STD.twd}, \code{DOY}.
#'   \item \code{ZG_phase}: point-level data with columns
#'         \code{TIME}, \code{dm}, \code{Phases}, \code{TWD}, \code{GRO}.
#' }
#'
#' @section Column definitions (ZG_cycle):
#' \describe{
#'   \item{Phases}{Integer; 1 = TWD (reversible shrinkage/expansion),
#'   2 = GRO (irreversible expansion).}
#'   \item{Start, End}{\code{POSIXct}; phase boundaries.}
#'   \item{Duration_h}{Numeric; phase duration in hours.}
#'   \item{Magnitude}{Numeric; GRO-only millimeter change of growth line across the phase. \code{NA} for TWD.}
#'   \item{rate}{Numeric; GRO-only rate in (\code{Magnitude*1000/Duration_h}).}
#'   \item{max.twd}{Numeric; peak TWD within the TWD phase.}
#'   \item{Max.twd.time}{\code{POSIXct}; time of \code{max.twd} within the TWD phase.}
#'   \item{AUC.load}{Numeric; pre-peak area under the TWD curve in mm*h, integrated from
#'   phase start to \code{Max.twd.time}.}
#'   \item{AUC.total}{Numeric; total area under the TWD curve in mm*h across the full TWD phase.}
#'   \item{ABr.value}{Numeric; Absolut Baumreaktion value for TWD phases,
#'   \deqn{\max(\mathrm{TWD}) \times \left(\frac{AUC_{load}}{\max(\mathrm{TWD})}\right)^{\beta}} \code{NA} for GRO.}
#'   \item{Avg.twd, STD.twd}{Numeric; mean and standard deviation of TWD in the phase.}
#'   \item{DOY}{Integer; day-of-year for the phase start.}
#' }
#'
#' @examples
#' \donttest{
#' library(dendRoAnalyst)
#' data(gf_nepa17)
#'
#' # Minimal example (no smoothing)
#' zg <- phase.zg(df = gf_nepa17[1:600, ], TreeNum = 1)
#' head(zg$ZG_cycle, 5)
#' head(zg$ZG_phase, 5)
#'
#' # With smoothing (e.g., 6 hours) to reduce short flips
#' zg6 <- phase.zg(df = gf_nepa17[1:600, ], TreeNum = 1, smoothing = 6, beta = 0.1)
#' subset(zg6$ZG_cycle, Phases == 1L)[1:5, c("Start","End","max.twd","ABr.value")]
#' }
#'
#' @importFrom lubridate ymd_hms ymd yday
#' @importFrom dplyr mutate rename select `%>%`
#' @importFrom tibble tibble
#' @importFrom stats median sd qnorm na.exclude
#' @importFrom moments kurtosis
#'
#' @export
phase.zg <- function(df, TreeNum, smoothing = NULL, beta = 0.1) {

  Duration_h <- NULL

  if (TreeNum > (ncol(df) - 1)) {
    stop("The tree number is not valid for the provided dataset.")
  }

  if (!is.numeric(beta) || length(beta) != 1 || is.na(beta) || beta < 0) {
    stop("beta must be a single non-negative numeric value.")
  }

  # -------------------------------------------------------
  # Helpers
  # -------------------------------------------------------
  parse_time_dm <- function(x) {
    if (inherits(x, "POSIXct")) return(as.POSIXct(x))
    if (inherits(x, "Date")) return(as.POSIXct(x))

    out <- suppressWarnings(lubridate::ymd_hms(x, quiet = TRUE))
    if (all(is.na(out))) {
      out_date <- suppressWarnings(lubridate::ymd(x, quiet = TRUE))
      if (!all(is.na(out_date))) {
        out <- as.POSIXct(out_date)
      }
    }
    if (all(is.na(out))) {
      out <- suppressWarnings(as.POSIXct(x))
    }
    out
  }

  cummax_na <- function(x) {
    out <- rep(NA_real_, length(x))
    current_max <- NA_real_

    for (i in seq_along(x)) {
      if (!is.na(x[i])) {
        if (is.na(current_max)) {
          current_max <- x[i]
        } else {
          current_max <- max(current_max, x[i])
        }
      }
      out[i] <- current_max
    }
    out
  }

  phase_cal_zg <- function(y) {
    all_max <- cummax_na(y)
    phases <- rep(NA_integer_, length(y))

    ok <- !is.na(y) & !is.na(all_max)
    phases[ok & y == all_max] <- 2L
    phases[ok & y <  all_max] <- 1L

    phases
  }

  reso_den <- function(input_time) {
    time1 <- as.POSIXct(input_time)
    dmin <- diff(as.numeric(time1)) / 60
    dmin <- dmin[is.finite(dmin) & dmin > 0]

    if (length(dmin) == 0) {
      warning("Could not infer temporal resolution from the time column.")
      return(NA_real_)
    }

    uniq <- unique(round(dmin, 6))
    if (length(uniq) > 1) {
      warning(
        "The temporal resolution of the dendrometer data is not fully consistent. ",
        "Results may be affected if the time series contains irregular spacing or missing intervals."
      )
    }

    stats::median(dmin)
  }

  trapz_time <- function(time_vec, value_vec) {
    if (length(time_vec) < 2 || length(value_vec) < 2) return(0)

    xh <- as.numeric(difftime(time_vec, time_vec[1], units = "hours"))
    keep <- is.finite(xh) & !is.na(value_vec)
    xh <- xh[keep]
    yv <- value_vec[keep]

    if (length(xh) < 2) return(0)

    sum(diff(xh) * (yv[-1] + yv[-length(yv)]) / 2)
  }

  phase_stats_zg <- function(dm_data, y_class) {

    gro_crv <- cummax_na(dm_data$dm)
    twd_crv <- gro_crv - dm_data$dm

    n <- nrow(dm_data)
    prev_y <- c(NA_integer_, y_class[-n])
    next_y <- c(y_class[-1], NA_integer_)

    start_idx <- which(!is.na(y_class) & (is.na(prev_y) | y_class != prev_y))
    end_idx   <- which(!is.na(y_class) & (is.na(next_y) | y_class != next_y))

    if (length(start_idx) == 0) {
      out_cycle <- tibble::tibble(
        Phases = integer(),
        Start = as.POSIXct(character()),
        End = as.POSIXct(character()),
        Duration_h = numeric(),
        Magnitude = numeric(),
        rate = numeric(),
        max.twd = numeric(),
        Max.twd.time = as.POSIXct(character()),
        AUC.load = numeric(),
        AUC.total = numeric(),
        ABr.value = numeric(),
        Avg.twd = numeric(),
        STD.twd = numeric(),
        DOY = integer()
      )

      out_phase <- dm_data %>%
        dplyr::mutate(
          Phases = y_class,
          TWD = twd_crv,
          GRO = gro_crv
        )

      return(list(out_cycle, tibble::as_tibble(out_phase)))
    }

    ph <- y_class[start_idx]
    strt_t <- dm_data$TIME[start_idx]

    # End is the boundary time (start of next phase), last phase ends at last timestamp
    boundary_idx <- pmin(end_idx + 1L, n)
    end_t <- dm_data$TIME[boundary_idx]

    dur_h <- as.numeric(difftime(end_t, strt_t, units = "hours"))

    # Growth magnitude over each non-overlapping phase segment
    magn <- rep(NA_real_, length(start_idx))
    gro_idx <- which(ph == 2L)
    if (length(gro_idx) > 0) {
      magn[gro_idx] <- gro_crv[end_idx[gro_idx]] - gro_crv[start_idx[gro_idx]]
    }

    rate <- rep(NA_real_, length(magn))
    ok_rate <- !is.na(magn) & !is.na(dur_h) & dur_h > 0
    rate[ok_rate] <- magn[ok_rate] * 1000 / dur_h[ok_rate]

    max_values <- rep(NA_real_, length(start_idx))
    max_time   <- rep(as.POSIXct(NA), length(start_idx))
    avg_twd    <- rep(NA_real_, length(start_idx))
    std_twd    <- rep(NA_real_, length(start_idx))
    auc_load   <- rep(NA_real_, length(start_idx))
    auc_total  <- rep(NA_real_, length(start_idx))
    abr_value  <- rep(NA_real_, length(start_idx))

    twd_idx <- which(ph == 1L)

    if (length(twd_idx) > 0) {
      for (k in twd_idx) {
        rng <- start_idx[k]:end_idx[k]
        twd_seg <- twd_crv[rng]
        time_seg <- dm_data$TIME[rng]

        if (!all(is.na(twd_seg))) {
          mx <- max(twd_seg, na.rm = TRUE)
          max_values[k] <- mx

          # last time of the maximum within phase
          wmx <- which(twd_seg == mx)
          max_time[k] <- time_seg[max(wmx)]

          avg_twd[k] <- mean(twd_seg, na.rm = TRUE)
          std_twd[k] <- stats::sd(twd_seg, na.rm = TRUE)

          peak_pos <- max(wmx)
          load_times <- c(strt_t[k], time_seg[seq_len(peak_pos)])
          load_vals  <- c(0, twd_seg[seq_len(peak_pos)])
          total_times <- c(strt_t[k], time_seg, end_t[k])
          total_vals  <- c(0, twd_seg, 0)

          auc_load[k] <- trapz_time(load_times, load_vals)
          auc_total[k] <- trapz_time(total_times, total_vals)

          if (!is.na(max_values[k]) && max_values[k] > 0 && !is.na(auc_load[k])) {
            abr_value[k] <- max_values[k] * ((auc_load[k] / max_values[k])^beta)
          }
        }
      }
    }

    dy <- lubridate::yday(strt_t)

    out_cycle <- tibble::tibble(
      Phases = ph,
      Start = strt_t,
      End = end_t,
      Duration_h = dur_h,
      Magnitude = magn,
      rate = rate,
      max.twd = max_values,
      Max.twd.time = as.POSIXct(max_time, origin = "1970-01-01", tz = attr(dm_data$TIME, "tzone")),
      AUC.load = auc_load,
      AUC.total = auc_total,
      ABr.value = abr_value,
      Avg.twd = avg_twd,
      STD.twd = std_twd,
      DOY = dy
    ) %>%
      dplyr::filter(Duration_h >= 0)

    out_phase <- dm_data %>%
      dplyr::mutate(
        Phases = y_class,
        TWD = twd_crv,
        GRO = gro_crv
      )

    list(out_cycle, tibble::as_tibble(out_phase))
  }

  # -------------------------------------------------------
  # Prepare input
  # -------------------------------------------------------
  df[[1]] <- parse_time_dm(df[[1]])
  if (all(is.na(df[[1]]))) {
    stop("Could not parse the first column as date-time.")
  }

  dm_data <- tibble::as_tibble(df) %>%
    dplyr::select(c(1, TreeNum + 1)) %>%
    dplyr::rename(
      TIME = 1,
      dm = 2
    )

  r.denro <- reso_den(dm_data$TIME)
  sf <- smoothing

  # -------------------------------------------------------
  # Optional smoothing for phase assignment
  # -------------------------------------------------------
  if (is.null(sf)) {
    y_for_phase <- dm_data$dm
  } else {
    if (!is.numeric(sf) || length(sf) != 1 || is.na(sf) || sf < 1 || sf > 24) {
      stop("smoothing must be a single numeric value between 1 and 24 hours.")
    }

    warning(
      paste0(
        "You are applying smoothing to raw dendrometer data. ",
        "The smoothing value is ", sf, " hour(s)."
      )
    )

    y_for_phase <- smooth_dm(
      time = dm_data$TIME,
      dm = dm_data$dm,
      method = "median_mean",
      window_hours = sf
    )
  }

  # -------------------------------------------------------
  # Classify phases and summarize
  # -------------------------------------------------------
  y_class <- phase_cal_zg(y_for_phase)
  ot <- phase_stats_zg(dm_data, y_class)

  out <- list(
    ZG_cycle = ot[[1]],
    ZG_phase = ot[[2]]
  )

  class(out) <- "ZG_output"
  out
}
