#' @title Truncation of dendrometer data
#'
#' @description
#' Truncates dendrometer data to a user-defined calendar-year and day-of-year
#' window.
#'
#' The first column of \code{df} is treated as the date-time column and is
#' renamed internally to \code{TIME}. The date-time column may be a
#' \code{Date}, \code{POSIXct}, or character vector in the format
#' \code{"yyyy-mm-dd HH:MM:SS"}. Character dates in the format
#' \code{"yyyy-mm-dd"} are also accepted.
#'
#' This version supports leap years correctly. For example, \code{DOY = 366}
#' is valid for leap years such as 2016, 2020, or 2024, but invalid for
#' non-leap years.
#'
#' @param df A data frame where the first column contains date or date-time
#'   values. The first column is renamed to \code{TIME} in the returned data.
#' @param CalYear Numeric value or numeric vector of length two giving the
#'   calendar year or start and end calendar years.
#' @param DOY Numeric value or numeric vector of length two giving the day of
#'   year or start and end day of year.
#'
#' @details
#' The truncation rules are:
#' \itemize{
#'   \item If \code{CalYear} has length one and \code{DOY} has length one,
#'     data from that single day are returned.
#'   \item If \code{CalYear} has length one and \code{DOY} has length two,
#'     data from the first DOY to the second DOY within the same year are
#'     returned.
#'   \item If \code{CalYear} has length two and \code{DOY} has length one,
#'     data from that DOY in the first year to the same DOY in the second year
#'     are returned.
#'   \item If \code{CalYear} has length two and \code{DOY} has length two,
#'     data from the first DOY of the first year to the second DOY of the
#'     second year are returned.
#' }
#'
#' The function uses real calendar dates internally rather than row positions.
#' Therefore, leap-year DOY 366 is handled correctly.
#'
#' @return
#' A tibble containing the truncated dendrometer data.
#'
#' @importFrom lubridate ymd_hms ymd
#' @importFrom dplyr rename filter select %>%
#' @importFrom tibble as_tibble
#'
#' @examples
#' \donttest{
#' library(dendRoAnalyst)
#' data(nepa)
#'
#' # Extract data from DOY 20 to 50 in 2017
#' trunc1 <- dendro.truncate(df = nepa, CalYear = 2017, DOY = c(20, 50))
#' head(trunc1, 10)
#'
#' # Leap-year example, if the data contain year 2020
#' # trunc2 <- dendro.truncate(df = df2020, CalYear = 2020, DOY = c(1, 366))
#' }
#'
#' @export
dendro.truncate <- function(df, CalYear, DOY) {
  .DATE <- NULL

  if (missing(df) || is.null(df)) {
    stop("'df' is missing or NULL.")
  }

  if (missing(CalYear) || is.null(CalYear) ||
      missing(DOY) || is.null(DOY)) {
    stop('Either "CalYear" and/or "DOY" are empty.')
  }

  cy <- CalYear
  doy <- DOY

  if (length(cy) < 1 || length(cy) > 2) {
    stop("The length of input 'CalYear' must be one or two.")
  }

  if (length(doy) < 1 || length(doy) > 2) {
    stop("The length of input 'DOY' must be one or two.")
  }

  if (!is.numeric(cy)) {
    stop("'CalYear' must be numeric.")
  }

  if (!is.numeric(doy)) {
    stop("'DOY' must be numeric.")
  }

  if (any(is.na(cy)) || any(is.na(doy))) {
    stop("'CalYear' and 'DOY' must not contain NA values.")
  }

  if (any(cy != floor(cy))) {
    stop("'CalYear' must contain whole years.")
  }

  if (any(doy != floor(doy))) {
    stop("'DOY' must contain whole day-of-year values.")
  }

  cy <- as.integer(cy)
  doy <- as.integer(doy)

  if (any(doy < 1 | doy > 366)) {
    stop("'DOY' values must be between 1 and 366.")
  }

  if (ncol(df) < 1) {
    stop("'df' must contain at least one column.")
  }

  ## Parse date-time column if needed
  if (!inherits(df[[1]], "Date") &&
      !inherits(df[[1]], "POSIXct") &&
      !inherits(df[[1]], "POSIXt")) {

    parsed_time <- lubridate::ymd_hms(df[[1]], quiet = TRUE)

    if (all(is.na(parsed_time))) {
      parsed_time <- lubridate::ymd(df[[1]], quiet = TRUE)
    }

    if (all(is.na(parsed_time))) {
      stop(
        "The first column could not be parsed as date-time. ",
        "Use format 'yyyy-mm-dd HH:MM:SS' or 'yyyy-mm-dd'."
      )
    }

    df[[1]] <- parsed_time
  }

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

  data$.DATE <- as.Date(data$TIME)

  if (any(is.na(data$.DATE))) {
    stop("Some values in the TIME column could not be converted to dates.")
  }

  available_years <- unique(as.integer(format(data$.DATE, "%Y")))

  for (yy in cy) {
    if (!yy %in% available_years) {
      stop("Provided year ", yy, " does not exist in dataset.")
    }
  }

  make_date_from_year_doy <- function(year, doy_value) {
    candidate <- as.Date(sprintf("%04d-01-01", year)) + doy_value - 1L

    if (as.integer(format(candidate, "%Y")) != year) {
      stop(
        "DOY ", doy_value, " is not valid for year ", year, ". ",
        "This usually means DOY 366 was requested for a non-leap year."
      )
    }

    candidate
  }

  if (length(cy) == 1 && length(doy) == 1) {
    start_date <- make_date_from_year_doy(cy[1], doy[1])
    end_date <- start_date

  } else if (length(cy) == 1 && length(doy) == 2) {
    start_date <- make_date_from_year_doy(cy[1], doy[1])
    end_date <- make_date_from_year_doy(cy[1], doy[2])

  } else if (length(cy) == 2 && length(doy) == 1) {
    start_date <- make_date_from_year_doy(cy[1], doy[1])
    end_date <- make_date_from_year_doy(cy[2], doy[1])

  } else {
    start_date <- make_date_from_year_doy(cy[1], doy[1])
    end_date <- make_date_from_year_doy(cy[2], doy[2])
  }

  if (end_date < start_date) {
    stop(
      "The requested truncation window is invalid: end date is before start date."
    )
  }

  out <- data %>%
    dplyr::filter(.DATE >= start_date, .DATE <= end_date) %>%
    dplyr::select(-.DATE)

  if (nrow(out) == 0) {
    stop("No data available for the selected CalYear/DOY window.")
  }

  out
}
