glue_together_range <- function(
  n,
  glue_template_1,
  glue_template_2
) {
  if (is.null(n) || all(is.na(n))) {
    return("")
  }
  n <-
    range(n, na.rm = TRUE) |>
    unique()

  if (all(n %in% c(Inf, -Inf))) {
    return("")
  }

  template <-
    if (length(n) == 1 && rlang::is_string(glue_template_1)) {
      glue_template_1
    } else if (rlang::is_string(glue_template_2)) {
      glue_template_2
    } else {
      ""
    }

  glue::glue(template)
}

#' Obtain range of N for a given data set and other settings.
#'
#'
#' @param data Dataset
#' @param dep,indep Character vector, names of (in)dependent variables
#' @param crowd String, one of "all", "target" or "others".
#' @param mesos_var Optional, NULL or string specifying name of variable used to
#'   split dataset.
#' @param mesos_group Optional, NULL or string specifying value in `mesos_var`
#' indicating the target group.
#' @param glue_template_1,glue_template_2 String, for the case of a single
#' value (1) or a range with minimum-maximum of values (2).
#'
#' @return Always a string.
#' @keywords internal
n_rng <- function(
  data,
  dep,
  indep = NULL,
  crowd = "all",
  mesos_var = NULL,
  mesos_group = NULL,
  glue_template_1 = "{n}",
  glue_template_2 = "[{n[1]}-{n[2]}]"
) {
  # Should always return a string, no matter the inputs
  # args <- check_options(call = match.call(),
  #                       ignore_args = .saros.env$ignore_args,
  #                       defaults_env = global_settings_get(fn_name = "n_rng"),
  #                       default_values = formals(n_rng))

  deps <- as.character(unique(dep))
  deps <- deps[!is.na(deps)]
  indeps <- as.character(unique(indep))
  indeps <- indeps[!is.na(indeps)]

  data <- data[
    makeme_keep_rows(
      data = data,
      crwd = crowd,
      mesos_var = mesos_var,
      mesos_group = mesos_group
    ),
    ,
    drop = FALSE
  ]

  n <-
    lapply(deps, function(d) {
      if (length(indeps) > 0) {
        lapply(indeps, function(i) {
          out <-
            dplyr::filter(
              data,
              dplyr::if_all(
                .cols = tidyselect::all_of(c(d, i)),
                .fns = ~ !is.na(.x)
              )
            ) |>
            nrow()
          return(out)
        }) |>
          unlist()
      } else {
        out <-
          dplyr::filter(
            data,
            dplyr::if_all(
              .cols = tidyselect::all_of(c(d)),
              .fns = ~ !is.na(.x)
            )
          ) |>
          nrow()
        return(out)
      }
    }) |>
    unlist()

  glue_together_range(
    n = n,
    glue_template_1 = glue_template_1,
    glue_template_2 = glue_template_2
  )
}

#' Provides a range (or single value) for N in data, given dep and indep
#'
#' @inheritParams n_rng
#' @param dep,indep Tidyselect syntax
#' @return String.
#' @export
#'
#' @examples n_range(data = ex_survey, dep = b_1:b_3, indep = x1_sex)
n_range <- function(
  data,
  dep,
  indep = NULL,
  mesos_var = NULL,
  mesos_group = NULL,
  glue_template_1 = "{n}",
  glue_template_2 = "[{n[1]}-{n[2]}]"
) {
  dep_enq <- rlang::enquo(arg = dep)
  dep_pos <- tidyselect::eval_select(dep_enq, data = data)
  indep_enq <- rlang::enquo(arg = indep)
  indep_pos <- tidyselect::eval_select(indep_enq, data = data)

  args <- check_options(
    call = match.call(),
    ignore_args = .saros.env$ignore_args,
    defaults_env = global_settings_get(fn_name = "n_rng"),
    default_values = formals(n_rng)
  )

  args$data <- data # reinsert after check_options
  args$dep <- names(dep_pos)
  args$indep <- names(indep_pos)
  n_rng(
    data = data,
    dep = args$dep,
    indep = args$indep,
    mesos_var = args$mesos_var,
    mesos_group = args$mesos_group,
    glue_template_1 = args$glue_template_1,
    glue_template_2 = args$glue_template_2
  )
}


#' Obtain range of N for a given `ggobj`.
#'
#'
#' Internal workhorse function for calculating N range from plot data
#'
#' @param data A data frame from a plot object
#' @param glue_template_1,glue_template_2 String, for the case of a single
#' value (1) or a range with minimum-maximum of values (2).
#'
#' @return Always a string.
#' @keywords internal
.n_rng2_impl <- function(
  data,
  glue_template_1 = "{n}",
  glue_template_2 = "[{n[1]}-{n[2]}]"
) {
  # Check if the plot has the .count_per_indep_group column
  # (added by makeme() for categorical plots)
  if (!".count_per_indep_group" %in% colnames(data)) {
    # Fallback: calculate N from the number of non-NA rows in the data
    # For int_plot_html plots, check .value (and indep if present)
    if (is_int_plot_html(data)) {
      # int_plot_html - calculate N per variable, then get range
      # Determine which columns to check for complete cases
      check_cols <- ".value"
      indep_cols <- setdiff(
        colnames(data),
        c(".variable_name", ".variable_label", ".value")
      )
      if (length(indep_cols) > 0) {
        check_cols <- c(check_cols, indep_cols)
      }

      # Calculate N per variable (group by .variable_name)
      if (".variable_name" %in% colnames(data)) {
        n_per_var <- tapply(
          seq_len(nrow(data)),
          data$.variable_name,
          function(idx) {
            sum(stats::complete.cases(data[idx, check_cols, drop = FALSE]))
          }
        )
        n <- range(n_per_var, na.rm = TRUE)
      } else {
        # Fallback if no .variable_name column
        n <- sum(stats::complete.cases(data[, check_cols, drop = FALSE]))
      }
    } else {
      # Other ggplot objects - use all columns
      n <- nrow(data[stats::complete.cases(data), , drop = FALSE])
    }
    return(glue_together_range(
      n = n,
      glue_template_1 = glue_template_1,
      glue_template_2 = glue_template_2
    ))
  }

  n <- suppressWarnings(unique(range(
    data$.count_per_indep_group,
    na.rm = TRUE
  )))

  # Check for infinite values (happens when all values are NA or vector is empty)
  if (any(is.infinite(n))) {
    cli::cli_warn(c(
      "No valid sample sizes found in {.code .count_per_indep_group}.",
      "i" = "All values are NA or the vector is empty.",
      "i" = "Returning 0 for N range."
    ))
    return("0")
  }

  glue_together_range(
    n = n,
    glue_template_1 = glue_template_1,
    glue_template_2 = glue_template_2
  )
}

#' Provides a range (or single value) for N in a plot object from `makeme()`
#'
#' @description
#' Takes a plot object from `makeme()` and returns the sample size (N) range as a
#' formatted string. Works with both `ggplot2` objects and `mschart` objects.
#'
#' @param plot_obj A plot object from `makeme()` - either a `ggplot2` object or an `ms_chart` object
#' @param ... Additional parameters passed to the specific method
#' @param glue_template_1,glue_template_2 String, for the case of a single
#'   value (1) or a range with minimum-maximum of values (2).
#'
#' @return String.
#' @export
#' @examples
#' # With ggplot2 (cat_plot_html)
#' n_range2(makeme(data = ex_survey, dep = b_1:b_3))
#'
#' # With mschart (cat_plot_docx)
#' \dontrun{
#' n_range2(
#'   makeme(data = ex_survey, dep = b_1:b_3,
#'          type = "cat_plot_docx", docx_return_object = TRUE)
#' )
#' }
n_range2 <- function(
  plot_obj,
  ...
) {
  # Manual dispatch to handle namespaced ggplot2 classes
  if (inherits(plot_obj, "gg") || inherits(plot_obj, "ggplot")) {
    return(n_range2.ggplot(plot_obj, ...))
  } else if (inherits(plot_obj, "ms_chart")) {
    return(n_range2.ms_chart(plot_obj, ...))
  } else {
    return(n_range2.default(plot_obj, ...))
  }
}

#' @export
#' @rdname n_range2
n_range2.ggplot <- function(
  plot_obj,
  glue_template_1 = "{n}",
  glue_template_2 = "[{n[1]}-{n[2]}]"
) {
  if (!ggplot2::is_ggplot(plot_obj)) {
    cli::cli_warn("{.arg plot_obj} must be a ggplot2-object, returning NULL.")
    return(NULL)
  }

  data <- plot_obj$data

  args <- check_options(
    call = match.call(),
    ignore_args = .saros.env$ignore_args,
    defaults_env = global_settings_get(fn_name = "n_rng"),
    default_values = formals(n_rng)
  )

  .n_rng2_impl(
    data = data,
    glue_template_1 = args$glue_template_1,
    glue_template_2 = args$glue_template_2
  )
}

#' @export
#' @rdname n_range2
n_range2.ms_chart <- function(
  plot_obj,
  glue_template_1 = "{n}",
  glue_template_2 = "[{n[1]}-{n[2]}]"
) {
  # Extract data from mschart object
  data <- plot_obj$data

  if (!(inherits(data, "data.frame") && nrow(data) > 0)) {
    cli::cli_warn(
      "{.arg plot_obj} must be an mschart object with a nrow>0 data in it. Returning NULL."
    )
    return(NULL)
  }

  args <- check_options(
    call = match.call(),
    ignore_args = .saros.env$ignore_args,
    defaults_env = global_settings_get(fn_name = "n_rng"),
    default_values = formals(n_rng)
  )

  .n_rng2_impl(
    data = data,
    glue_template_1 = args$glue_template_1,
    glue_template_2 = args$glue_template_2
  )
}

#' @export
#' @rdname n_range2
n_range2.default <- function(plot_obj, ...) {
  cli::cli_abort(
    c(
      "{.arg n_range2} requires a plot object from {.fn makeme}.",
      "i" = "Received object of class: {.cls {class(plot_obj)}}",
      "i" = "Expected: {.cls gg} (from {.code type='cat_plot_html'}) or {.cls ms_chart} (from {.code type='cat_plot_docx', docx_return_object=TRUE})"
    )
  )
}
