# ---------- display-label relabelling ----------
#' @keywords internal
#' @noRd
.relabel_display <- function(display_df,
                             variable_labels = NULL,
                             level_labels    = NULL) {
  df <- as.data.frame(display_df, stringsAsFactors = FALSE)
  stopifnot(all(c("Characteristic","is_header") %in% names(df)))

  .must_be_named_character(variable_labels, "variable_labels", allow_null = TRUE)
  if (!is.null(level_labels) &&
      (!is.list(level_labels) || is.null(names(level_labels)) ||
       any(!nzchar(names(level_labels))))) {
    stop("`level_labels` must be a named list.", call. = FALSE)
  }
  if (!is.null(level_labels)) {
    for (nm in names(level_labels)) {
      .must_be_named_character(level_labels[[nm]], paste0("level_labels$", nm), allow_null = FALSE)
    }
  }

  # carry variable name down so we can match levels
  raw_exposure <- attr(display_df, "row_exposure", exact = TRUE)
  if (!is.null(raw_exposure) && length(raw_exposure) == nrow(df)) {
    var_id <- as.character(raw_exposure)
  } else {
    var_id <- character(nrow(df))
    cur <- NA_character_
    for (i in seq_len(nrow(df))) {
      if (isTRUE(df$is_header[i])) cur <- trimws(df$Characteristic[i])
      var_id[i] <- cur
    }
  }

  # variable (header) relabel
  if (!is.null(variable_labels)) {
    hdr_idx <- which(df$is_header %in% TRUE)
    if (length(hdr_idx)) {
      old_hdr <- var_id[hdr_idx]
      idx <- match(old_hdr, names(variable_labels))
      repl <- variable_labels[idx]
      df$Characteristic[hdr_idx] <- ifelse(is.na(idx), df$Characteristic[hdr_idx], unname(repl))
    }
  }

  # level relabel (keep indent)
  if (!is.null(level_labels)) {
    lvl_idx <- which(df$is_header %in% FALSE)
    if (length(lvl_idx)) {
      base_lvl <- sub("^\\s+", "", df$Characteristic[lvl_idx])
      df$Characteristic[lvl_idx] <- mapply(
        FUN = function(v, lv, original) {
          ll <- level_labels[[v]]
          if (is.null(ll)) return(original)
          hit <- match(lv, names(ll))
          if (is.na(hit)) return(original)
          paste0("  ", unname(ll[hit]))
        },
        v = var_id[lvl_idx], lv = base_lvl, original = df$Characteristic[lvl_idx],
        SIMPLIFY = TRUE, USE.NAMES = FALSE
      )
    }
  }

  .attach_display_metadata(
    df,
    row_exposure = var_id,
    variable_labels = attr(display_df, "variable_labels", exact = TRUE)
  )
}

#' Validate named character vector inputs
#' @keywords internal
#' @noRd
.must_be_named_character <- function(x, arg, allow_null = TRUE) {
  if (is.null(x) && isTRUE(allow_null)) {
    return(invisible(TRUE))
  }
  if (!is.character(x) || is.null(names(x)) || any(!nzchar(names(x)))) {
    stop("`", arg, "` must be a named character vector.", call. = FALSE)
  }
  invisible(TRUE)
}

#' Normalize common header aliases to visible table_display names
#' @keywords internal
#' @noRd
.normalize_header_labels <- function(header_labels, display_df, approach = NULL) {
  .must_be_named_character(header_labels, "header_labels", allow_null = TRUE)
  if (is.null(header_labels)) {
    return(NULL)
  }

  out <- header_labels
  effect_cols <- setdiff(names(display_df), c("Characteristic", "is_header", "N", "p-value"))
  effect_col <- if (length(effect_cols) == 1L) effect_cols else NA_character_
  aliases <- c(
    estimate = effect_col,
    p.value = "p-value",
    p_value = "p-value",
    pvalue = "p-value",
    N = "N"
  )

  for (nm in names(out)) {
    if (!nm %in% names(display_df) && nm %in% names(aliases) && !is.na(aliases[[nm]])) {
      names(out)[names(out) == nm] <- aliases[[nm]]
    }
  }

  bad <- setdiff(names(out), names(display_df))
  if (length(bad)) {
    stop(
      "`header_labels` contains columns not found in the table: ",
      paste(bad, collapse = ", "),
      call. = FALSE
    )
  }

  out
}

#' Drop display columns while preserving row metadata
#' @keywords internal
#' @noRd
.drop_display_columns <- function(display_df, cols) {
  cols <- intersect(cols, names(display_df))
  if (!length(cols)) {
    return(display_df)
  }

  out <- display_df[, setdiff(names(display_df), cols), drop = FALSE]
  attr(out, "row_exposure") <- attr(display_df, "row_exposure", exact = TRUE)
  attr(out, "variable_labels") <- attr(display_df, "variable_labels", exact = TRUE)
  out
}

#' Check for stratified regression output classes
#' @keywords internal
#' @noRd
.is_stratified_regression_output <- function(tbl) {
  inherits(tbl, "stratified_uni_reg") ||
    inherits(tbl, "stratified_multi_reg") ||
    inherits(tbl, "stratified_cox_reg") ||
    inherits(tbl, "stratified_surv_reg")
}

#' Pull a shared stratified effect label from header aliases
#' @keywords internal
#' @noRd
.stratified_effect_label <- function(header_labels, default) {
  if (is.null(header_labels)) {
    return(default)
  }

  allowed <- c("estimate", default)
  bad <- setdiff(names(header_labels), allowed)
  if (length(bad)) {
    stop(
      "`header_labels` contains columns not found in the stratified table: ",
      paste(bad, collapse = ", "),
      call. = FALSE
    )
  }

  if ("estimate" %in% names(header_labels)) {
    return(unname(header_labels[["estimate"]]))
  }
  if (default %in% names(header_labels)) {
    return(unname(header_labels[[default]]))
  }

  default
}

#' Rebuild a modified stratified table with the original strata builders
#' @keywords internal
#' @noRd
.rebuild_modified_stratified_table <- function(tbl, display_df, footnotes, header_labels = NULL) {
  if (!.is_stratified_regression_output(tbl)) {
    return(NULL)
  }

  spanners <- tbl$levels
  theme <- if (!is.null(tbl$theme)) tbl$theme else "minimal"
  format <- tolower(tbl$format)

  if (inherits(tbl, "stratified_uni_reg")) {
    effect_label <- .stratified_effect_label(header_labels, .get_effect_label(tbl$approach))
    if (identical(format, "gt")) {
      return(.build_gt_strata_wide_uni(display_df, spanners, effect_label, theme, footnotes))
    }
    return(.build_flex_strata_wide_uni(display_df, spanners, effect_label, theme, footnotes))
  }

  if (inherits(tbl, "stratified_multi_reg")) {
    effect_label <- .stratified_effect_label(
      header_labels,
      paste("Adjusted", .get_effect_label(tbl$approach))
    )
    if (identical(format, "gt")) {
      return(.build_gt_strata_wide_multi(display_df, spanners, effect_label, theme, footnotes))
    }
    return(.build_flex_strata_wide_multi(display_df, spanners, effect_label, theme, footnotes))
  }

  if (inherits(tbl, "stratified_cox_reg")) {
    effect_label <- .stratified_effect_label(
      header_labels,
      if (isTRUE(tbl$adjusted_mode) || isTRUE(tbl$multivariable)) {
      "Adjusted HR (95% CI)"
    } else {
      "HR (95% CI)"
      }
    )
    builder <- if (isTRUE(tbl$adjusted_mode) || isTRUE(tbl$multivariable)) {
      if (identical(format, "gt")) .build_gt_strata_wide_multi else .build_flex_strata_wide_multi
    } else {
      if (identical(format, "gt")) .build_gt_strata_wide_uni else .build_flex_strata_wide_uni
    }
    return(builder(display_df, spanners, effect_label, theme, footnotes))
  }

  if (inherits(tbl, "stratified_surv_reg")) {
    effect_label <- .stratified_effect_label(
      header_labels,
      if (isTRUE(tbl$adjusted_mode) || isTRUE(tbl$multivariable)) {
      "Adjusted Time Ratio (95% CI)"
    } else {
      "Time Ratio (95% CI)"
      }
    )
    builder <- if (isTRUE(tbl$adjusted_mode) || isTRUE(tbl$multivariable)) {
      if (identical(format, "gt")) .build_gt_strata_wide_multi else .build_flex_strata_wide_multi
    } else {
      if (identical(format, "gt")) .build_gt_strata_wide_uni else .build_flex_strata_wide_uni
    }
    return(builder(display_df, spanners, effect_label, theme, footnotes))
  }

  NULL
}

#' Validate logical scalar inputs
#' @keywords internal
#' @noRd
.must_be_flag <- function(x, arg) {
  if (!is.logical(x) || length(x) != 1L || is.na(x)) {
    stop("`", arg, "` must be TRUE or FALSE.", call. = FALSE)
  }
  invisible(TRUE)
}

# ---------- main: modify labels/headers/notes and rebuild (gtregression only) ----------
#' Modify Regression/Descriptive Tables (labels, headers, caption, notes)
#'
#' Works with objects created by this package (class \code{"gtregression"}):
#' \code{uni_reg()}, \code{multi_reg()}, \code{cox_reg()}, \code{surv_reg()},
#' stratified regression outputs, \code{descriptive_table()}, and
#' \code{merge_tables()}.
#' No \pkg{gtsummary} dependency or fallback.
#'
#' @param gt_table Table object produced by this package (must contain
#'   \code{$table_display}).
#' @param variable_labels Named character vector, for example
#'   \code{c(old_var = "New label", ...)}.
#' @param level_labels Named list for factor levels:
#'   \code{list(var1 = c(old = "New", ...), var2 = c(...))}.
#' @param header_labels Named character vector to rename visible headers, e.g.
#'   \code{c("OR (95\% CI)" = "Crude OR", "p-value" = "P")}. Common aliases
#'   such as \code{estimate}, \code{p.value}, and \code{N} are also accepted.
#' @param caption Optional caption/title.
#' @param bold_labels Logical; bold variable (header) rows in the body. Defaults
#'   to \code{TRUE} to preserve the package table hierarchy.
#' @param bold_levels Logical; bold factor level rows in the body.
#' @param italic_labels Logical; italicize variable (header) rows in the body.
#' @param italic_levels Logical; italicize factor level rows in the body.
#' @param remove_N Logical; if \code{TRUE}, drops displayed \code{N} columns
#'   from univariable and stratified package tables. For stratified survival
#'   outputs, event columns are retained unless the original table was created
#'   with \code{show_sample = "none"}.
#' @param remove_N_obs Logical; if \code{TRUE}, suppresses multivariable
#'   complete-case footnote.
#' @param remove_abbreviations Logical; if \code{TRUE}, removes the
#'   Abbreviations footnote line.
#' @param remove_adjustment_note Logical; if \code{TRUE}, removes the automatic
#'   \code{Adjusted for ...} footnote. Use \code{caveat} to add customised
#'   wording.
#' @param caveat Optional extra footnote.
#' @return The modified table object (same class as input).
#'
#' @examples
#' birthwt_data <- data_birthwt |>
#'   dplyr::mutate(
#'     smoke = factor(smoke, levels = c(0, 1), labels = c("No", "Yes")),
#'     ht = factor(ht, levels = c(0, 1), labels = c("No", "Yes")),
#'     low = factor(low, levels = c(0, 1), labels = c("Normal BW", "Low BW"))
#'   )
#'
#' tbl <- uni_reg(
#'   data = birthwt_data,
#'   outcome = "low",
#'   exposures = c("age", "smoke", "ht"),
#'   approach = "logit",
#'   format = "gt"
#' )
#'
#' modify_table(
#'   tbl,
#'   variable_labels = c(age = "Maternal age", smoke = "Smoking"),
#'   level_labels = list(smoke = c(Yes = "Smoker")),
#'   header_labels = c(estimate = "Crude OR", p.value = "P"),
#'   caption = "Univariable regression for low birth weight"
#' )$table
#' @export
modify_table <- function(gt_table,
                         variable_labels = NULL,
                         level_labels = NULL,
                         header_labels = NULL,
                         caption = NULL,
                         bold_labels = TRUE,
                         bold_levels = FALSE,
                         italic_labels = FALSE,
                         italic_levels = FALSE,
                         remove_N = FALSE,
                         remove_N_obs = FALSE,
                         remove_abbreviations = FALSE,
                         remove_adjustment_note = FALSE,
                         caveat = NULL) {
  tbl <- gt_table

  # --------- accept only our objects ----------
  is_pkg_obj <- inherits(tbl, "gtregression") && !is.null(tbl$table_display)
  if (!is_pkg_obj) {
    stop("`modify_table()` expects a gtregression object from this package (with $table_display).",
         call. = FALSE)
  }

  .must_be_named_character(variable_labels, "variable_labels", allow_null = TRUE)
  .must_be_named_character(header_labels, "header_labels", allow_null = TRUE)
  .must_be_flag(bold_labels, "bold_labels")
  .must_be_flag(bold_levels, "bold_levels")
  .must_be_flag(italic_labels, "italic_labels")
  .must_be_flag(italic_levels, "italic_levels")
  .must_be_flag(remove_N, "remove_N")
  .must_be_flag(remove_N_obs, "remove_N_obs")
  .must_be_flag(remove_abbreviations, "remove_abbreviations")
  .must_be_flag(remove_adjustment_note, "remove_adjustment_note")
  if (!is.null(caption) && (!is.character(caption) || length(caption) != 1L || is.na(caption))) {
    stop("`caption` must be NULL or a single character string.", call. = FALSE)
  }
  if (!is.null(caveat) && (!is.character(caveat) || length(caveat) != 1L || is.na(caveat))) {
    stop("`caveat` must be NULL or a single character string.", call. = FALSE)
  }

  # --------- relabel display safely ----------
  tbl$table_display <- .relabel_display(
    tbl$table_display,
    variable_labels = variable_labels,
    level_labels    = level_labels
  )
  if (!is.null(variable_labels)) {
    current_labels <- tbl$variable_labels
    if (is.null(current_labels)) current_labels <- character(0)
    current_labels[names(variable_labels)] <- unname(variable_labels)
    tbl$variable_labels <- current_labels
    attr(tbl$table_display, "variable_labels") <- current_labels
  }

  # --------- optionally drop N columns ----------
  if (isTRUE(remove_N)) {
    n_cols <- names(tbl$table_display)[
      names(tbl$table_display) == "N" |
        startsWith(names(tbl$table_display), "..N__")
    ]
    tbl$table_display <- .drop_display_columns(tbl$table_display, n_cols)
  }

  if (!.is_stratified_regression_output(tbl)) {
    header_labels <- .normalize_header_labels(header_labels, tbl$table_display, tbl$approach)
  }

  # --------- collect/compose footnotes ----------
  footnotes <- character(0)
  # Existing package objects store their complete display notes in `$footnotes`.
  # Retain these first so a presentation-only modification never loses them.
  if (!is.null(tbl$footnotes)) footnotes <- c(footnotes, tbl$footnotes)
  # Some older objects attach notes to the object or rendered inner table.
  ft_attr1 <- attr(tbl, "footnotes", exact = TRUE)
  if (!is.null(ft_attr1)) footnotes <- c(footnotes, ft_attr1)
  ft_attr2 <- if (!is.null(tbl$table)) attr(tbl$table, "footnotes", exact = TRUE) else NULL
  if (!is.null(ft_attr2)) footnotes <- c(footnotes, ft_attr2)
  # abbreviations line (unless removed)
  if (!isTRUE(remove_abbreviations) && !is.null(tbl$approach) &&
      exists(".abbrev_note", mode = "function")) {
    footnotes <- c(footnotes, .abbrev_note(tbl$approach))
  }
  if (isTRUE(remove_abbreviations)) {
    footnotes <- footnotes[!grepl("^Abbreviations:", footnotes)]
  }
  # multivariable N note (unless removed)
  if (!isTRUE(remove_N_obs) && inherits(tbl, "multi_reg")) {
    if (!is.null(tbl$models) && !is.null(tbl$models$multivariable_model)) {
      n_used <- tryCatch(stats::nobs(tbl$models$multivariable_model),
                         error = function(e) NA_integer_)
      if (!is.na(n_used)) {
        footnotes <- c(footnotes,
                       paste0("N = ", n_used,
                              " complete observations included in the model."))
      }
    }
  }
  if (isTRUE(remove_N_obs)) {
    footnotes <- footnotes[!grepl("^N = .*complete observations", footnotes)]
  }
  # default descriptive notes (if none)
  if (!length(footnotes) && inherits(tbl, "descriptive_table")) {
    footnotes <- c(
      "Categorical variables shown as n (%).",
      "Continuous variables shown as Median (IQR)."
    )
  }
  # Keep the multivariable adjustment note aligned with the labels currently
  # visible in the table, including adjustment variables not shown as rows.
  if (inherits(tbl, c("multi_reg", "cox_reg", "surv_reg")) &&
      !is.null(tbl$adjust_for) &&
      length(tbl$adjust_for) && exists(".display_adjustment_note", mode = "function")) {
    footnotes <- footnotes[!grepl("^Adjusted for ", footnotes)]
    footnotes <- c(
      footnotes,
      .display_adjustment_note(tbl$adjust_for, tbl$variable_labels)
    )
  }
  if (isTRUE(remove_adjustment_note)) {
    footnotes <- footnotes[!grepl("^Adjusted for ", footnotes)]
  }
  # user caveat last
  if (!is.null(caveat) && nzchar(caveat)) footnotes <- c(footnotes, caveat)
  footnotes <- unique(footnotes[nzchar(footnotes)])
  tbl$footnotes <- footnotes
  if (!is.null(caption)) {
    tbl$caption <- caption
  }

  # --------- reorder visible columns for univariate: N → Effect → p-value ----------
  reorder_univariate_cols <- function(display_df) {
    stopifnot(all(c("Characteristic","is_header") %in% names(display_df)))
    cols <- names(display_df)
    body_cols <- setdiff(cols, c("Characteristic","is_header"))
    if (length(body_cols)) {
      # classic uni layout has exactly one effect col + optional N + p-value
      hasN   <- "N" %in% body_cols
      hasP   <- "p-value" %in% body_cols
      effect <- setdiff(body_cols, c("N","p-value"))
      if (hasP && length(effect) == 1L) {
        ord <- c("Characteristic", if (hasN) "N", effect, "p-value")
        return(display_df[, ord, drop = FALSE])
      }
    }
    # otherwise: drop is_header and keep as-is
    display_df[, setdiff(names(display_df), "is_header"), drop = FALSE]
  }

  # --------- builders (gt / flextable) ----------
  .build_pkg_gt <- function(display_df, header_labels, footnotes) {
    stopifnot(requireNamespace("gt", quietly = TRUE))
    out <- reorder_univariate_cols(display_df)

    merged_labels <- NULL
    if (inherits(tbl, "merged_table") && !is.null(tbl$column_labels)) {
      merged_labels <- c(
        Characteristic = "Characteristic",
        unlist(tbl$column_labels, use.names = TRUE)
      )
      merged_labels <- merged_labels[names(merged_labels) %in% names(out)]
    }

    lab_values <- if (!is.null(merged_labels)) merged_labels else header_labels
    if (!is.null(header_labels)) {
      lab_values[names(header_labels)] <- unname(header_labels)
    }
    lab_list <- if (!is.null(lab_values)) as.list(lab_values) else NULL
    names(lab_list) <- names(lab_values)

    tb <- gt::gt(out) |>
      gt::cols_label(.list = lab_list) |>
      gt::cols_align("left",   columns = "Characteristic") |>
      gt::cols_align("center", columns = setdiff(names(out), "Characteristic")) |>
      gt::sub_missing(columns = setdiff(names(out), "Characteristic"), missing_text = "") |>
      gt::tab_options(
        table.font.names         = "system-ui",
        data_row.padding         = gt::px(4),
        heading.background.color = "white",
        table.background.color   = "white",
        source_notes.padding     = gt::px(2)
      )

    # bold column labels
    tb <- gt::tab_style(tb, gt::cell_text(weight = "bold"),
                        locations = gt::cells_column_labels())
    # bold variable headers (on demand; note: many tables already bold by default)
    if (isTRUE(bold_labels)) {
      hdr_idx <- which(display_df$is_header %in% TRUE)
      if (length(hdr_idx)) {
        tb <- gt::tab_style(tb, gt::cell_text(weight = "bold"),
                            locations = gt::cells_body(rows = hdr_idx, columns = "Characteristic"))
      }
    }
    # optional bold levels
    if (isTRUE(bold_levels)) {
      lvl_idx <- which(display_df$is_header %in% FALSE)
      if (length(lvl_idx)) {
        tb <- gt::tab_style(tb, gt::cell_text(weight = "bold"),
                            locations = gt::cells_body(rows = lvl_idx, columns = "Characteristic"))
      }
    }
    # indent levels
    tb <- gt::tab_style(tb, gt::cell_text(indent = gt::px(12)),
                        locations = gt::cells_body(rows = display_df$is_header %in% FALSE,
                                                   columns = "Characteristic"))
    if (inherits(tbl, "merged_table") && !is.null(tbl$spanners) &&
        !is.null(tbl$column_labels)) {
      start <- 2L
      for (i in seq_along(tbl$spanners)) {
        cols_i <- names(tbl$column_labels[[i]])
        cols_i <- cols_i[cols_i %in% names(out)]
        k <- length(cols_i)
        if (k > 0) {
          tb <- gt::tab_spanner(
            tb,
            label = tbl$spanners[i],
            columns = names(out)[start:(start + k - 1L)]
          )
          start <- start + k
        }
      }
      tb <- gt::tab_style(tb, gt::cell_text(weight = "bold"),
                          locations = gt::cells_column_spanners())
    }
    # caption
    if (!is.null(caption)) tb <- gt::tab_caption(tb, caption = caption)
    # footnotes
    if (length(footnotes)) {
      tb <- gt::tab_source_note(tb, source_note = gt::md(paste(footnotes, collapse = "  \n")))
    }
    tb
  }

  .build_pkg_flex <- function(display_df, header_labels, footnotes) {
    stopifnot(requireNamespace("flextable", quietly = TRUE))
    out <- reorder_univariate_cols(display_df)

    # NA -> "" (older flextable compat)
    out[] <- lapply(out, function(x) { x[is.na(x)] <- ""; x })

    merged_labels <- NULL
    if (inherits(tbl, "merged_table") && !is.null(tbl$column_labels)) {
      merged_labels <- c(
        Characteristic = "Characteristic",
        unlist(tbl$column_labels, use.names = TRUE)
      )
      merged_labels <- merged_labels[names(merged_labels) %in% names(out)]
    }

    labels <- stats::setNames(names(out), names(out))
    if (!is.null(merged_labels)) {
      labels[names(merged_labels)] <- unname(merged_labels)
    }
    if (!is.null(header_labels)) {
      labels[names(header_labels)] <- unname(header_labels)
    }

    ft <- flextable::flextable(out)
    ft <- flextable::set_header_labels(ft, values = labels)
    if (inherits(tbl, "merged_table") && !is.null(tbl$spanners) &&
        !is.null(tbl$column_labels)) {
      widths <- c(
        1L,
        vapply(tbl$column_labels, function(x) {
          sum(names(x) %in% names(out))
        }, integer(1))
      )
      keep <- c(TRUE, widths[-1] > 0)
      ft <- flextable::add_header_row(
        ft,
        values = c("", tbl$spanners)[keep],
        colwidths = widths[keep]
      )
    }
    ft <- flextable::align(ft, j = "Characteristic", align = "left",  part = "all")
    ft <- flextable::align(ft, j = setdiff(names(out), "Characteristic"), align = "center", part = "all")
    ft <- flextable::bold(ft, part = "header", bold = TRUE)

    # bold variable headers (on demand)
    if (isTRUE(bold_labels)) {
      hdr_idx <- which(display_df$is_header %in% TRUE)
      if (length(hdr_idx)) ft <- flextable::bold(ft, i = hdr_idx, j = "Characteristic", bold = TRUE, part = "body")
    }
    # optional bold levels
    if (isTRUE(bold_levels)) {
      lvl_idx <- which(display_df$is_header %in% FALSE)
      if (length(lvl_idx)) ft <- flextable::bold(ft, i = lvl_idx, j = "Characteristic", bold = TRUE, part = "body")
    }
    # indent levels
    lvl_idx <- which(display_df$is_header %in% FALSE)
    if (length(lvl_idx)) ft <- flextable::padding(ft, i = lvl_idx, j = "Characteristic",
                                                  padding.left = 14, part = "body")
    # caption
    if (!is.null(caption)) ft <- flextable::set_caption(ft, caption = caption)
    # footnotes
    if (length(footnotes)) {
      ft <- flextable::add_footer_lines(ft, values = footnotes)
      ft <- flextable::align(ft, part = "footer", align = "left")
      ft <- flextable::padding(ft, part = "footer", padding.top = 0, padding.bottom = 0)
      ft <- flextable::line_spacing(ft, part = "footer", space = 0.9)
    }

    ft <- flextable::autofit(ft)
    ft
  }

  apply_body_text_style <- function(rendered, display_df) {
    header_rows <- which(display_df$is_header %in% TRUE)
    level_rows <- which(display_df$is_header %in% FALSE)

    if (inherits(rendered, "gt_tbl")) {
      if (length(header_rows)) {
        rendered <- gt::tab_style(
          rendered,
          style = gt::cell_text(
            weight = if (isTRUE(bold_labels)) "bold" else "normal",
            style = if (isTRUE(italic_labels)) "italic" else "normal"
          ),
          locations = gt::cells_body(rows = header_rows, columns = "Characteristic")
        )
      }
      if (length(level_rows)) {
        rendered <- gt::tab_style(
          rendered,
          style = gt::cell_text(
            weight = if (isTRUE(bold_levels)) "bold" else "normal",
            style = if (isTRUE(italic_levels)) "italic" else "normal"
          ),
          locations = gt::cells_body(rows = level_rows, columns = "Characteristic")
        )
      }
      return(rendered)
    }

    if (inherits(rendered, "flextable")) {
      if (length(header_rows)) {
        rendered <- flextable::bold(
          rendered, i = header_rows, j = "Characteristic",
          bold = isTRUE(bold_labels), part = "body"
        )
        rendered <- flextable::italic(
          rendered, i = header_rows, j = "Characteristic",
          italic = isTRUE(italic_labels), part = "body"
        )
      }
      if (length(level_rows)) {
        rendered <- flextable::bold(
          rendered, i = level_rows, j = "Characteristic",
          bold = isTRUE(bold_levels), part = "body"
        )
        rendered <- flextable::italic(
          rendered, i = level_rows, j = "Characteristic",
          italic = isTRUE(italic_levels), part = "body"
        )
      }
    }

    rendered
  }

  # --------- rebuild rendered table ----------
  strata_table <- .rebuild_modified_stratified_table(tbl, tbl$table_display, footnotes, header_labels)
  if (!is.null(strata_table)) {
    tbl$table <- strata_table
  } else if (identical(tolower(tbl$format), "gt") || inherits(tbl$table, "gt_tbl")) {
    tbl$table <- .build_pkg_gt(tbl$table_display, header_labels, footnotes)
  } else if (identical(tolower(tbl$format), "flextable") || inherits(tbl$table, "flextable")) {
    tbl$table <- .build_pkg_flex(tbl$table_display, header_labels, footnotes)
  }

  tbl$table <- apply_body_text_style(tbl$table, tbl$table_display)

  tbl
}
