#' @title Calculate relative dendrometer change during adverse climate periods and the following normal phase
#'
#' @description
#' Identifies adverse climate periods from a climate time series and calculates
#' relative dendrometer change from the start of each adverse phase.
#'
#' In addition to the adverse phase itself, the function also defines a
#' \emph{complete period} for each event ID:
#' \itemize{
#'   \item the adverse climate phase, and
#'   \item the following normal climate phase until the next adverse phase starts
#'   (or until the end of the common series).
#' }
#'
#' Relative dendrometer change is always calculated from the first day of the
#' adverse phase for that ID.
#'
#' The function returns:
#' \itemize{
#'   \item \code{adverse_change}: relative change only during adverse phases,
#'   \item \code{normal_change}: relative change only during the following normal phases,
#'   \item \code{full_period_change}: relative change across adverse + following normal phase,
#'   \item \code{continuous_full_period_change}: same as \code{full_period_change},
#'   but values do not reset to 0 at the next adverse phase and instead continue
#'   from the end of the previous full period,
#'   \item \code{period_info}: per-ID and per-tree summary metrics.
#' }
#'
#' @details
#' The algorithm works as follows:
#' \enumerate{
#'   \item Climate data are thresholded to identify adverse days.
#'   \item Consecutive adverse days are grouped into candidate adverse runs.
#'   \item Only runs satisfying \code{thresholdDays} are retained as true adverse events.
#'   \item For each retained adverse event, the following normal period is defined
#'   as all non-adverse days until the next adverse event starts (or the end of
#'   the series).
#'   \item Dendrometer data are resampled to daily resolution using
#'   \code{\link[dendRoAnalyst:dendro.resample]{dendro.resample}}.
#'   \item Relative dendrometer change is calculated from the first day of the
#'   adverse phase for each ID.
#' }
#'
#' \strong{Threshold syntax:}
#' \itemize{
#'   \item single-threshold forms such as \code{"<5"}, \code{"<=10"},
#'   \code{">3"}, \code{">=0"}, \code{"==0"},
#'   \item range forms for \code{thresholdClim} such as \code{"between(5,10)"}
#'   or \code{"5:10"}, both interpreted inclusively.
#' }
#'
#' \code{thresholdDays} must use a single-threshold form such as \code{">5"}.
#'
#' @references
#' Raffelsbauer V, Spannl S, Peña K, Pucha-Cofrep D, Steppe K, Bräuning A (2019).
#' Tree Circumference Changes and Species-Specific Growth Recovery After Extreme Dry Events
#' in a Montane Rainforest in Southern Ecuador.
#' \emph{Frontiers in Plant Science}, 10, 342.
#' \doi{10.3389/fpls.2019.00342}
#'
#' @param df A data frame with the first column containing date-time stamps
#'   (\code{yyyy-mm-dd HH:MM:SS}, convertible to \code{POSIXct}) and subsequent
#'   columns containing dendrometer series.
#' @param Clim A data frame with the first column containing dates and the second
#'   column containing the climate variable of interest.
#' @param dailyValue Character. Daily aggregation statistic used when resampling
#'   dendrometer data. One of \code{"max"}, \code{"min"}, \code{"mean"}, or
#'   \code{"sum"}. Default is \code{"max"}.
#' @param thresholdClim Character string defining the climate threshold.
#'   Supported forms are:
#'   \itemize{
#'     \item \code{"<5"}, \code{"<=5"}, \code{">5"}, \code{">=5"}, \code{"==5"}
#'     \item \code{"between(5,10)"}
#'     \item \code{"5:10"}
#'   }
#' @param thresholdDays Character string defining the minimum duration threshold
#'   for consecutive adverse days. Must use a single-threshold form such as
#'   \code{">5"} or \code{">=3"}.
#' @param showPlot Logical. If \code{TRUE}, diagnostic plots are produced and the
#'   user can interactively choose IDs and phase type (\code{"adverse"},
#'   \code{"normal"}, or \code{"both"}). Default is \code{TRUE}.
#'
#' @return
#' A list of class \code{"clim_twd_output"} containing:
#' \describe{
#'   \item{adverse_change}{A data frame of relative dendrometer change during
#'   adverse phases only.}
#'   \item{normal_change}{A data frame of relative dendrometer change during the
#'   following normal phases only.}
#'   \item{full_period_change}{A data frame of relative dendrometer change across
#'   adverse + following normal phase, reset to 0 at each adverse start.}
#'   \item{continuous_full_period_change}{A data frame similar to
#'   \code{full_period_change}, but values continue from the end of the previous
#'   full period instead of resetting to 0.}
#'   \item{period_info}{A data frame with one row per ID and tree, containing
#'   adverse and normal phase boundaries, the date and magnitude of maximum
#'   adverse decline during the adverse phase, lags to first drop below zero, lags
#'   to maximum adverse decline, and lags to return to zero.}
#'   \item{phase_table}{A data frame with adverse and normal phase boundaries for
#'   each ID.}
#' }
#'
#' @note
#' If the dendrometer and climate datasets have different time spans, analysis is
#' automatically restricted to their common overlap.
#'
#' @seealso
#' \code{\link[dendRoAnalyst:dendro.resample]{dendro.resample}},
#' \code{\link{phase.zg}},
#' \code{\link{phase.sc}}
#'
#' @examples
#' \donttest{
#' data(gf_nepa17)
#' data(ktm_rain17)
#'
#' rel_out <- clim.twd(
#'   df = gf_nepa17,
#'   Clim = ktm_rain17,
#'   dailyValue = "max",
#'   thresholdClim = "<10",
#'   thresholdDays = ">5",
#'   showPlot = FALSE
#' )
#'
#' head(rel_out$adverse_change)
#' head(rel_out$normal_change)
#' head(rel_out$full_period_change)
#' head(rel_out$continuous_full_period_change)
#' head(rel_out$period_info)
#'
#' rel_out2 <- clim.twd(
#'   df = gf_nepa17,
#'   Clim = ktm_rain17,
#'   thresholdClim = "between(5,10)",
#'   thresholdDays = ">3",
#'   showPlot = FALSE
#' )
#' }
#'
#' @importFrom lubridate ymd_hms ymd
#' @importFrom dplyr mutate group_by summarise ungroup rename across select
#'   filter arrange bind_rows all_of left_join first transmute %>%
#' @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 scale_fill_manual
#'
#' @export
clim.twd <- function(df, Clim, dailyValue = "max",
                     thresholdClim = "<10", thresholdDays = ">5",
                     showPlot = TRUE) {

  TIME <- DATE <- IDs <- climate <- phase_type <- Variable <- Value <- NULL
  adverse_ID <- adverse_start <- adverse_end <- has_normal_phase <- normal_start <- NULL
  normal_end <- xmin <- xmax <- NULL

  parse_threshold <- function(x, arg_name) {
    x <- trimws(x)

    if (grepl("^between\\s*\\(\\s*-?[0-9.]+\\s*,\\s*-?[0-9.]+\\s*\\)$",
              x, ignore.case = TRUE)) {
      nums <- gsub("^between\\s*\\(|\\)$", "", x, ignore.case = TRUE)
      nums <- strsplit(nums, ",")[[1]]
      nums <- as.numeric(trimws(nums))
      if (length(nums) != 2 || any(is.na(nums))) {
        stop("Invalid '", arg_name, "' range. Use for example 'between(5,10)'.")
      }
      lo <- min(nums)
      hi <- max(nums)
      return(list(type = "between", lower = lo, upper = hi))
    }

    if (grepl("^\\s*-?[0-9.]+\\s*:\\s*-?[0-9.]+\\s*$", x)) {
      nums <- strsplit(x, ":")[[1]]
      nums <- as.numeric(trimws(nums))
      if (length(nums) != 2 || any(is.na(nums))) {
        stop("Invalid '", arg_name, "' range. Use for example '5:10'.")
      }
      lo <- min(nums)
      hi <- max(nums)
      return(list(type = "between", lower = lo, upper = hi))
    }

    mm <- regexec("^\\s*(<=|>=|==|<|>)\\s*(-?[0-9.]+)\\s*$", x)
    rr <- regmatches(x, mm)[[1]]

    if (length(rr) == 3) {
      return(list(
        type = "single",
        op = rr[2],
        value = as.numeric(rr[3])
      ))
    }

    stop(
      "'", arg_name, "' must look like '<5', '<=5', '>5', '>=5', '==5', ",
      "'between(5,10)', or '5:10'."
    )
  }

  apply_threshold <- function(z, thr) {
    if (thr$type == "between") {
      return(z >= thr$lower & z <= thr$upper)
    }

    switch(
      thr$op,
      "<"  = z < thr$value,
      "<=" = z <= thr$value,
      ">"  = z > thr$value,
      ">=" = z >= thr$value,
      "==" = z == thr$value
    )
  }

  find_first_below_zero <- function(x) {
    ii <- which(x < 0)
    if (length(ii) == 0) return(NA_integer_)
    ii[1]
  }

  find_return_to_zero <- function(x, start_idx) {
    if (is.na(start_idx)) return(NA_integer_)
    ii <- which(seq_along(x) > start_idx & x >= 0)
    if (length(ii) == 0) return(NA_integer_)
    ii[1]
  }

  if (!inherits(df[[1]], "Date") && !inherits(df[[1]], "POSIXct")) {
    df[[1]] <- lubridate::ymd_hms(df[[1]])
  }
  if (!inherits(Clim[[1]], "Date") && !inherits(Clim[[1]], "POSIXct")) {
    Clim[[1]] <- lubridate::ymd(Clim[[1]])
  }

  df <- dendro.resample(df = df, by = "D", value = dailyValue)
  df <- df %>% dplyr::mutate(TIME = as.Date(TIME))

  clim <- tibble::as_tibble(Clim) %>%
    dplyr::rename(DATE = 1, climate = 2)

  ctime <- intersect(df$TIME, clim$DATE)
  if (length(ctime) == 0) {
    stop("The dendrometer and climate data do not have a common time period.")
  } else {
    if (!((length(ctime) == nrow(df)) && (length(ctime) == nrow(clim)))) {
      message("The analyses period is reduced to common time period.")
      common_t <- as.Date(ctime, origin = "1970-01-01", tz = "UTC")
      df   <- df   %>% dplyr::filter(TIME %in% common_t)
      clim <- clim %>% dplyr::filter(DATE %in% common_t)
    }
  }

  tree_cols <- names(df)[names(df) != "TIME"]

  thr_clim <- parse_threshold(thresholdClim, "thresholdClim")
  thr_days <- parse_threshold(thresholdDays, "thresholdDays")

  if (thr_days$type != "single") {
    stop("'thresholdDays' must use a single-threshold form such as '>5' or '>=3'.")
  }

  adverse_flag <- apply_threshold(clim$climate, thr_clim)

  rr <- rle(adverse_flag)
  adverse_ids <- integer(length(adverse_flag))
  adverse_start_flag <- rep(FALSE, length(adverse_flag))

  pos <- 1L
  id_counter <- 0L

  for (i in seq_along(rr$lengths)) {
    run_len <- rr$lengths[i]
    run_val <- rr$values[i]
    idx <- pos:(pos + run_len - 1L)

    keep_run <- isTRUE(run_val) && isTRUE(apply_threshold(run_len, thr_days))

    if (keep_run) {
      id_counter <- id_counter + 1L
      adverse_ids[idx] <- id_counter
      adverse_start_flag[idx[1]] <- TRUE
    }

    pos <- pos + run_len
  }

  clim <- clim %>%
    dplyr::mutate(
      adverse_start = adverse_start_flag,
      adverse_ID = adverse_ids
    )

  if (max(adverse_ids, na.rm = TRUE) == 0) {
    empty_out <- list(
      adverse_change = tibble::tibble(),
      normal_change = tibble::tibble(),
      full_period_change = tibble::tibble(),
      continuous_full_period_change = tibble::tibble(),
      period_info = tibble::tibble(),
      phase_table = tibble::tibble()
    )
    class(empty_out) <- "clim_twd_output"

    if (showPlot) {
      message("No adverse periods found for the given thresholds.")
      p1 <- ggplot2::ggplot(clim, ggplot2::aes(x = DATE, y = climate)) +
        ggplot2::geom_area(fill = "lightblue") +
        ggplot2::theme_minimal() +
        ggplot2::labs(
          title = "Climate plot",
          x = "Date",
          y = "Climate value"
        )
      print(p1)
    }

    return(empty_out)
  }

  phase_table <- clim %>%
    dplyr::filter(adverse_ID > 0) %>%
    dplyr::group_by(adverse_ID) %>%
    dplyr::summarise(
      adverse_start = min(DATE),
      adverse_end = max(DATE),
      n_days_adverse = dplyr::n(),
      .groups = "drop"
    ) %>%
    dplyr::arrange(adverse_ID) %>%
    dplyr::rename(IDs = adverse_ID)

  next_start <- c(phase_table$adverse_start[-1], as.Date(NA))
  series_last_date <- max(df$TIME, na.rm = TRUE)

  phase_table$normal_start <- phase_table$adverse_end + 1
  phase_table$full_end <- as.Date(ifelse(
    is.na(next_start),
    as.character(series_last_date),
    as.character(as.Date(next_start) - 1)
  ))
  phase_table$has_normal_phase <- phase_table$normal_start <= phase_table$full_end
  phase_table$normal_end <- ifelse(
    phase_table$has_normal_phase,
    as.character(phase_table$full_end),
    NA_character_
  )
  phase_table$normal_end <- as.Date(phase_table$normal_end)

  full_id <- integer(nrow(clim))
  phase_type_vec <- rep(NA_character_, nrow(clim))

  for (i in seq_len(nrow(phase_table))) {
    id_i <- phase_table$IDs[i]
    a_start <- phase_table$adverse_start[i]
    a_end <- phase_table$adverse_end[i]
    f_end <- phase_table$full_end[i]

    idx_full <- which(clim$DATE >= a_start & clim$DATE <= f_end)
    idx_adv  <- which(clim$DATE >= a_start & clim$DATE <= a_end)
    idx_norm <- which(clim$DATE > a_end & clim$DATE <= f_end)

    full_id[idx_full] <- id_i
    phase_type_vec[idx_adv] <- "adverse"
    if (length(idx_norm) > 0) phase_type_vec[idx_norm] <- "normal"
  }

  clim <- clim %>%
    dplyr::mutate(
      IDs = full_id,
      phase_type = phase_type_vec
    )

  df <- dplyr::left_join(
    df,
    clim %>% dplyr::select(DATE, IDs, phase_type),
    by = c("TIME" = "DATE")
  )

  full_period_change <- df %>%
    dplyr::filter(IDs > 0) %>%
    dplyr::arrange(IDs, TIME) %>%
    dplyr::group_by(IDs) %>%
    dplyr::mutate(
      dplyr::across(
        dplyr::all_of(tree_cols),
        ~ . - dplyr::first(.)
      )
    ) %>%
    dplyr::ungroup()

  adverse_change <- full_period_change %>%
    dplyr::filter(phase_type == "adverse")

  normal_change <- full_period_change %>%
    dplyr::filter(phase_type == "normal")

  continuous_list <- list()
  offset_vals <- stats::setNames(rep(0, length(tree_cols)), tree_cols)

  for (i in seq_len(nrow(phase_table))) {
    id_i <- phase_table$IDs[i]

    seg_df <- full_period_change %>%
      dplyr::filter(IDs == id_i) %>%
      dplyr::arrange(TIME)

    if (nrow(seg_df) == 0) next

    cont_df <- seg_df
    for (cc in tree_cols) {
      cont_df[[cc]] <- seg_df[[cc]] + offset_vals[[cc]]
      offset_vals[[cc]] <- cont_df[[cc]][nrow(cont_df)]
    }

    continuous_list[[i]] <- cont_df
  }

  continuous_full_period_change <- dplyr::bind_rows(continuous_list)

  period_info_list <- list()

  for (i in seq_len(nrow(phase_table))) {
    id_i <- phase_table$IDs[i]
    a_start <- phase_table$adverse_start[i]
    a_end <- phase_table$adverse_end[i]
    n_start <- phase_table$normal_start[i]
    n_end <- phase_table$normal_end[i]
    f_end <- phase_table$full_end[i]

    seg_df <- full_period_change %>%
      dplyr::filter(IDs == id_i) %>%
      dplyr::arrange(TIME)

    adv_df <- adverse_change %>%
      dplyr::filter(IDs == id_i) %>%
      dplyr::arrange(TIME)

    cont_df <- continuous_full_period_change %>%
      dplyr::filter(IDs == id_i) %>%
      dplyr::arrange(TIME)

    for (cc in tree_cols) {
      x_full <- seg_df[[cc]]
      t_full <- seg_df$TIME

      x_adv <- adv_df[[cc]]
      t_adv <- adv_df$TIME

      x_cont <- cont_df[[cc]]

      below_idx <- find_first_below_zero(x_full)
      return_idx <- find_return_to_zero(x_full, below_idx)

      if (length(x_adv) == 0 || all(is.na(x_adv))) {
        min_adv_idx <- NA_integer_
        min_adv_val <- NA_real_
      } else {
        min_adv_idx <- which.min(x_adv)
        min_adv_val <- x_adv[min_adv_idx]
      }

      no_adverse_tree <- is.na(below_idx)

      period_info_list[[length(period_info_list) + 1L]] <- tibble::tibble(
        IDs = id_i,
        tree = cc,
        adverse_start = a_start,
        adverse_end = a_end,
        normal_start = if (phase_table$has_normal_phase[i]) n_start else as.Date(NA),
        normal_end = if (phase_table$has_normal_phase[i]) n_end else as.Date(NA),
        full_end = f_end,
        no_adverse_for_tree = no_adverse_tree,
        first_below_zero_date = if (is.na(below_idx)) as.Date(NA) else as.Date(t_full[below_idx]),
        lag_to_below_zero = if (is.na(below_idx)) NA_integer_ else as.integer(t_full[below_idx] - a_start),
        max_adverse_decline_value = min_adv_val,
        max_adverse_decline_date = if (is.na(min_adv_idx)) as.Date(NA) else as.Date(t_adv[min_adv_idx]),
        lag_to_max_adverse_decline = if (is.na(min_adv_idx)) NA_integer_ else as.integer(t_adv[min_adv_idx] - a_start),
        return_to_zero_date = if (is.na(return_idx)) as.Date(NA) else as.Date(t_full[return_idx]),
        lag_to_return_zero = if (is.na(return_idx)) NA_integer_ else as.integer(t_full[return_idx] - a_start),
        change_end_adverse = if (length(x_adv) == 0) NA_real_ else x_adv[length(x_adv)],
        change_end_full_period = if (length(x_full) == 0) NA_real_ else x_full[length(x_full)],
        continuous_start_full_period = if (length(x_cont) == 0) NA_real_ else x_cont[1],
        continuous_end_full_period = if (length(x_cont) == 0) NA_real_ else x_cont[length(x_cont)]
      )
    }
  }

  period_info <- dplyr::bind_rows(period_info_list)

  if (showPlot) {
    phase_plot_df <- dplyr::bind_rows(
      phase_table %>%
        dplyr::transmute(
          IDs,
          xmin = adverse_start,
          xmax = adverse_end,
          phase_type = "adverse"
        ),
      phase_table %>%
        dplyr::filter(has_normal_phase) %>%
        dplyr::transmute(
          IDs,
          xmin = normal_start,
          xmax = normal_end,
          phase_type = "normal"
        )
    )

    p1 <- ggplot2::ggplot(clim, ggplot2::aes(x = DATE, y = climate, group = 1)) +
      ggplot2::geom_area(fill = "lightblue") +
      ggplot2::geom_rect(
        data = phase_plot_df,
        ggplot2::aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = phase_type),
        inherit.aes = FALSE,
        alpha = 0.18
      ) +
      ggplot2::geom_text(
        data = phase_table,
        ggplot2::aes(x = adverse_start, y = max(clim$climate, na.rm = TRUE), label = IDs),
        inherit.aes = FALSE
      ) +
      ggplot2::scale_fill_manual(values = c(adverse = "firebrick2", normal = "darkseagreen3")) +
      ggplot2::theme_minimal() +
      ggplot2::labs(
        title = "Climate plot with adverse and following normal phases",
        x = "Date",
        y = "Climate value",
        fill = "Phase"
      )
    print(p1)

    print("The period IDs are listed below:")
    print(unique(full_period_change$IDs))
    print("Please choose period IDs from the list. Press Enter twice when done.")
    perd <- scan()

    if (length(perd) == 0) {
      perd <- phase_table$IDs[1]
    }

    ptype <- readline("Choose phase type: 'adverse', 'normal', or 'both' [both]: ")
    if (ptype == "") ptype <- "both"
    ptype <- tolower(trimws(ptype))

    if (!(ptype %in% c("adverse", "normal", "both"))) {
      warning("Invalid phase type. Using 'both'.")
      ptype <- "both"
    }

    if (ptype == "adverse") {
      fdf <- adverse_change %>% dplyr::filter(IDs %in% perd)
    } else if (ptype == "normal") {
      fdf <- normal_change %>% dplyr::filter(IDs %in% perd)
    } else {
      fdf <- full_period_change %>% dplyr::filter(IDs %in% perd)
    }

    if (nrow(fdf) == 0) {
      message("No rows available for the selected IDs and phase type.")
    } else {
      data_long <- tidyr::pivot_longer(
        fdf,
        cols = dplyr::all_of(tree_cols),
        names_to = "Variable",
        values_to = "Value"
      )

      p2 <- ggplot2::ggplot(data_long, ggplot2::aes(x = TIME, y = Value, color = Variable))

      if (ptype == "both") {
        phase_plot_df2 <- dplyr::bind_rows(
          phase_table %>%
            dplyr::filter(IDs %in% perd) %>%
            dplyr::transmute(
              IDs,
              xmin = adverse_start,
              xmax = adverse_end,
              phase_type = "adverse"
            ),
          phase_table %>%
            dplyr::filter(IDs %in% perd, has_normal_phase) %>%
            dplyr::transmute(
              IDs,
              xmin = normal_start,
              xmax = normal_end,
              phase_type = "normal"
            )
        )

        p2 <- p2 +
          ggplot2::geom_rect(
            data = phase_plot_df2,
            ggplot2::aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = phase_type),
            inherit.aes = FALSE,
            alpha = 0.12
          ) +
          ggplot2::scale_fill_manual(values = c(adverse = "firebrick2", normal = "darkseagreen3"))
      }

      p2 <- p2 +
        ggplot2::geom_line() +
        ggplot2::geom_point() +
        ggplot2::labs(
          title = paste("Relative dendrometer change:", ptype, "phase"),
          x = "Time",
          y = "Relative change from adverse-phase start",
          color = "Trees",
          fill = "Phase"
        ) +
        ggplot2::theme_minimal() +
        ggplot2::theme(
          legend.position = "bottom",
          axis.title = ggplot2::element_text(size = 14),
          axis.text = ggplot2::element_text(size = 12)
        )

      if (length(perd) > 1) {
        p2 <- p2 + ggplot2::facet_wrap(~ IDs, scales = "free_x", ncol = 1)
      }

      print(p2)
    }
  }

  out <- list(
    adverse_change = adverse_change,
    normal_change = normal_change,
    full_period_change = full_period_change,
    continuous_full_period_change = continuous_full_period_change,
    period_info = period_info,
    phase_table = phase_table
  )

  class(out) <- "clim_twd_output"
  out
}
