#' Multivariable regression
#'
#' Fit adjusted or multivariable regression models and return a manuscript-style
#' table in \pkg{flextable} or \pkg{gt} format.
#'
#' @param data A \code{data.frame} containing the variables of interest.
#' @param outcome Character scalar; name of the outcome variable. Quoted and
#'   bare names are accepted.
#' @param exposures Character vector; exposure variable(s) to report.
#'   Quoted names are recommended in scripts, and bare names are also accepted.
#'   If \code{adjust_for = NULL}, all exposures are included in a single
#'   multivariable model. If \code{adjust_for} is supplied, one adjusted
#'   model is fitted per exposure and only exposure-specific adjusted
#'   estimate(s) are displayed.
#' @param adjust_for Optional character vector of adjustment variables. Quoted
#'   and bare names are accepted. Must not overlap with \code{exposures}.
#' @param interaction Optional character scalar specifying one interaction term
#'   using standard formula syntax, e.g. \code{"bmi*sex"}.
#'   When used with \code{adjust_for}, only a single exposure should be supplied.
#' @param approach Character scalar specifying the regression approach.
#'   One of \code{"logit"}, \code{"firth"}, \code{"logbinomial"}, \code{"poisson"},
#'   \code{"linear"}, \code{"robpoisson"}, or \code{"negbin"}.
#'   Use \code{"firth"} for Firth penalized logistic regression, especially
#'   with sparse cells or separation.
#' @param format Output table format; one of \code{"flextable"} (default) or
#'   \code{"gt"}.
#' @param theme Table styling preset (e.g. \code{"minimal"}, \code{"striped"},
#'   \code{"clinical"}, \code{"shaded"}, \code{"jama"}) or a character vector of
#'   primitives such as \code{c("plain","zebra","lines","labels_bold","compact","header_shaded")}.
#' @param model_stats Logical; if \code{TRUE}, extract model-fit statistics
#'   such as AIC, BIC, log-likelihood, deviance, pseudo R-squared for
#'   non-linear models, and R-squared for linear models. Statistics are stored
#'   in the returned object's \code{model_stats} element and are not added to
#'   the publication table.
#' @param show_ref Logical; if \code{TRUE} (default), display reference-category
#'   rows as \code{"Ref."}. If \code{FALSE}, hide reference rows; a message
#'   reminds users to use \code{show_ref = TRUE} when reference rows are needed.
#'
#' @details
#' Use the default mode when you want all exposures in one model. Use
#' \code{adjust_for} when you want one adjusted estimate per reported exposure,
#' with the same adjustment set used repeatedly.
#'
#' If exposure variables have a \code{"label"} attribute, for example from
#' \code{labelled::var_label()}, those labels are used automatically in the
#' displayed table and plots. Internal matching still uses the original column
#' names.
#'
#' @return A list of class \code{c("gtregression","multi_reg", ...)} with elements:
#' \describe{
#'   \item{table}{A \code{flextable} (if \code{format="flextable"}) or
#'   \code{gt_tbl} (if \code{format="gt"}).}
#'   \item{table_body}{A data frame of adjusted estimates and confidence intervals
#'   for each exposure and level.}
#'   \item{table_display}{A data frame used for rendering the final table,
#'   including header and level rows.}
#'   \item{models}{A list of fitted model(s).}
#'   \item{model_summaries}{\code{summary()} output for the fitted model(s).}
#'   \item{model_stats}{Model-fit statistics when \code{model_stats = TRUE};
#'   otherwise \code{NULL}.}
#'   \item{variable_labels}{Named character vector of display labels used for
#'   exposure variables.}
#'   \item{reg_check}{Regression diagnostics for linear models. Printing this
#'   element renders a publication-ready table; individual diagnostic rows remain
#'   available for programmatic use. Other approaches return a message.}
#'   \item{approach}{The regression approach used.}
#'   \item{format}{The output format used.}
#'   \item{source}{Function identifier (\code{"multi_reg"}).}
#'   \item{adjusted_mode}{Whether one adjusted model per exposure was fitted.}
#'   \item{adjust_for}{Adjustment variables supplied by the user, if any.}
#'   \item{exposures}{Exposure variables supplied by the user.}
#'   \item{interaction}{Interaction term supplied by the user, if any.}
#' }
#'
#' @details
#' In default mode (\code{adjust_for = NULL}), all exposures are included in a
#' single multivariable model. In adjusted mode, one model is fitted per exposure,
#' adjusting for the variables specified in \code{adjust_for}.
#'
#' Interaction terms specified via \code{interaction} are included in the model
#' using standard formula expansion (e.g. \code{bmi*sex}). Interaction effects are
#' displayed as additional rows beneath the corresponding exposure.
#'
#' @examples
#' endometrial_data <- data_endometrial
#' endometrial_data$HG <- factor(endometrial_data$HG, levels = c(0, 1))
#' endometrial_data$NV <- factor(endometrial_data$NV, levels = c(0, 1))
#' multi_reg(endometrial_data, HG, c(NV, PI, EH), approach = firth, format = gt)$table
#'
#' @importFrom stats qnorm residuals nobs
#' @export
multi_reg <- function(data,
                      outcome,
                      exposures,
                      adjust_for = NULL,
                      interaction = NULL,
                      approach = "logit",
                      format = c("flextable", "gt"),
                      theme = c("minimal"),
                      model_stats = FALSE,
                      show_ref = TRUE) {

  outcome <- .vars_arg(substitute(outcome), env = parent.frame(), data_names = names(data))
  exposures <- .vars_arg(substitute(exposures), env = parent.frame(), data_names = names(data))
  adjust_for <- .vars_arg(
    substitute(adjust_for),
    env = parent.frame(),
    allow_null = TRUE,
    data_names = names(data)
  )
  interaction <- .interaction_arg(substitute(interaction), env = parent.frame(), allow_null = TRUE)
  approach <- .choice_arg(
    substitute(approach),
    env = parent.frame(),
    choices = c("logit","firth","logbinomial","poisson","robpoisson","linear","negbin")
  )
  approach <- .normalize_approach(approach)
  format <- .choice_arg(substitute(format), env = parent.frame(), choices = c("flextable","gt"))
  theme <- .choice_arg(substitute(theme), env = parent.frame())
  if (!is.logical(model_stats) || length(model_stats) != 1L || is.na(model_stats)) {
    stop("`model_stats` must be TRUE or FALSE.", call. = FALSE)
  }
  .validate_show_ref(show_ref)

  format <- match.arg(format, c("flextable","gt"))
  theme <- .resolve_theme(theme)

  fmt_class <- if (format == "gt") "gt_multi" else "ft_multi"
  effect_label_adj <- paste("Adjusted", .get_effect_label(approach))
  variable_labels <- .var_label_map(
    data,
    unique(c(exposures, adjust_for, .interaction_vars(interaction)))
  )

  core <- .run_multi_core(
    data = data,
    outcome = outcome,
    exposures = exposures,
    approach = approach,
    adjust_for = adjust_for,
    interaction = interaction
  )
  core$table_body <- .format_interaction_levels(
    core$table_body, core$data_clean, variable_labels
  )

  footnotes <- if (!core$adjusted_mode) {
    c(
      .abbrev_note(approach),
      if (isTRUE(show_ref) && any(core$table_body$ref %in% TRUE)) .ref_note() else NULL,
      if (!is.null(interaction)) .interaction_note(interaction) else NULL,
      if (!is.na(core$n_used)) {
        paste0(
          "N = ", core$n_used,
          " complete observations included in the model."
        )
      } else {
        "N reflects complete observations included in the model."
      }
    )
  } else {
    n_note <- if (length(unique(stats::na.omit(core$n_used))) == 1L) {
      paste0(
        "N = ", unique(stats::na.omit(core$n_used)),
        " complete observations included in each adjusted model."
      )
    } else {
      paste0(
        "N varies across adjusted models according to complete observations for the outcome, ",
        "current exposure, and adjustment variables."
      )
    }

    c(
      .abbrev_note(approach),
      if (isTRUE(show_ref) && any(core$table_body$ref %in% TRUE)) .ref_note() else NULL,
      .display_adjustment_note(adjust_for, variable_labels),
      if (!is.null(interaction)) .interaction_note(interaction) else NULL,
      n_note
    )
  }

  .message_hidden_ref_rows("multi_reg", core$table_body, show_ref)
  display_df <- .make_display_multi(
    core$table_body,
    core$data_clean,
    outcome,
    effect_label_adj,
    variable_labels = variable_labels,
    show_ref = show_ref
  )
  .must_be_display_df_multi(display_df)

  tbl <- if (format == "gt") {
    .build_gt_multi(display_df, effect_label_adj, footnotes, theme)
  } else {
    .build_flextable_multi(display_df, effect_label_adj, footnotes, theme)
  }

  res <- list(
    table = tbl,
    table_body = core$table_body,
    table_display = display_df,
    models = core$models,
    model_summaries = core$model_summaries,
    model_stats = if (isTRUE(model_stats)) .model_stats_table(core$models, approach) else NULL,
    variable_labels = variable_labels,
    footnotes = footnotes,
    reg_check = .as_reg_check_result(core$reg_check, format = format),
    approach = approach,
    format = format,
    source = "multi_reg",
    adjusted_mode = core$adjusted_mode,
    adjust_for = if (isTRUE(core$adjusted_mode)) unique(adjust_for) else NULL,
    exposures = unique(exposures),
    interaction = interaction,
    show_ref = isTRUE(show_ref)
  )

  class(res) <- c("gtregression", "multi_reg", fmt_class, class(res))
  res
}
