#' @title Cross-validated ANOVA for O-PLS models
#' @description
#' Performs a cross-validated ANOVA (CV-ANOVA) test for OPLS models. The function compares residuals from a null model and a model using cross-validated predictive scores to assess the significance of the OPLS model.
#'
#' @param smod An object of class \code{m8_model}, generated by function \pkg{opls} in the \pkg{metabom8} package.
#'
#' @details
#' \strong{Interpretation of the CV-ANOVA table}
#'
#' The CV-ANOVA compares two nested linear models:
#' \itemize{
#'   \item \emph{Null model}: \eqn{Y = \beta_0 + \epsilon}
#'   \item \emph{Full model}: \eqn{Y = \beta_0 + \beta_1 t_{\text{pred,cv}} + \epsilon}
#' }
#'
#' where \eqn{t_{\text{pred,cv}}} represents the cross-validated predictive component
#' score(s) obtained from the OPLS model.
#'
#' The ANOVA table contains:
#' \itemize{
#'   \item \strong{SS} – Sum of Squares
#'   \item \strong{DF} – Degrees of Freedom
#'   \item \strong{MS} – Mean Squares (SS / DF)
#'   \item \strong{F_value} – F statistic comparing model vs. null
#'   \item \strong{p_value} – P-value from the F-test
#' }
#'
#' The \emph{Regression} row quantifies the reduction in residual variance
#' achieved by including the cross-validated predictive score(s).
#' The \emph{Residual} row represents the unexplained variance of the full model.
#'
#' The F-statistic is computed as:
#' \deqn{
#' F = \frac{(RSS_0 - RSS_1) / df_{reg}}{RSS_1 / df_{res}}
#' }
#' where \eqn{RSS_0} and \eqn{RSS_1} are the residual sums of squares of the
#' null and full models, respectively.
#'
#' \strong{Predictive interpretation}
#'
#' A small p-value (typically < 0.05) indicates that the cross-validated
#' predictive score(s) significantly reduce residual variance compared to an
#' intercept-only model. This suggests that the OPLS model captures
#' statistically meaningful predictive structure in \eqn{Y}.
#'
#' A large p-value indicates that the predictive component does not explain
#' \eqn{Y} significantly better than chance, implying weak or unstable
#' predictive performance.
#'
#' Importantly, CV-ANOVA evaluates the \emph{linear explanatory power of the
#' cross-validated predictive scores}, not the descriptive separation of the
#' latent space. A model may show visual class separation or moderate R\eqn{^2}
#' yet fail CV-ANOVA if predictive performance is weak.
#'
#' Sample size strongly affects statistical power. With small \eqn{n}, even
#' models with moderate predictive strength may not reach statistical
#' significance.
#'
#' CV-ANOVA is intended for continuous response (regression) models.
#'
#' @return A \code{data.frame} containing:
#' \itemize{
#'   \item \code{SS} - Sum of Squares
#'   \item \code{DF} - Degrees of Freedom
#'   \item \code{MS} - Mean Squares
#'   \item \code{F_value} - F statistic
#'   \item \code{p_value} - P-value from the F-test
#' }
#'
#' @references
#' Eriksson, L., et al. (2008). CV-ANOVA for significance testing of PLS and OPLS models. \emph{Journal of Chemometrics}, 22(11-12), 594–600.
#'
#'
#' @examples
#' data("covid")
#'
#' X <- covid$X
#' Y <- as.numeric(factor(covid$an$type)) - 1
#'
#' scaling <- uv_scaling(center=TRUE)
#' cv <- balanced_mc(10, split=2/3, type='R', probs = c(0, 0.5, 1))
#' mod <- opls(X, Y, scaling, cv)
#'
#' cv_anova(mod)
#'
#' @family model_validation
#' @export
#' @importFrom stats lm pf residuals
cv_anova <- function(smod) {
  if (!(inherits(smod, "m8_model") && smod@engine == "opls" && smod@ctrl$type == "R")) {
    stop("Requires an m8_model with engine == 'opls' and type regression", call. = FALSE)
  }

  # pick response: numeric Y preferred; otherwise first dummy column
  y <- NULL
  if (!is.null(smod@fit$Y) && is.numeric(smod@fit$Y)) {
    y <- as.numeric(smod@fit$Y)
  } else {
    stop("Could not find a usable response in smod@fit$Y.", call. = FALSE)
  }

  tcv <- scores(smod, cv=TRUE)
  if (length(y) != length(tcv)) stop("Length mismatch between Y and t_pred_cv.", call. = FALSE)

  n <- length(y)

  m0 <- stats::lm(y ~ 1)
  m1 <- stats::lm(y ~ 1 + tcv)

  rss0 <- sum(residuals(m0)^2)
  rss1 <- sum(residuals(m1)^2)

  ss_reg <- rss0 - rss1
  ss_res <- rss1
  ss_tot <- rss0

  df_tot <- n - 1
  df_res <- n - 2
  df_reg <- df_tot - df_res  # = 1

  ms_reg <- ss_reg / df_reg
  ms_res <- ss_res / df_res

  F_val <- ms_reg / ms_res
  p_val <- stats::pf(F_val, df_reg, df_res, lower.tail = FALSE)

  out <- data.frame(
    SS = c(ss_tot, ss_reg, ss_res, NA_real_),
    DF = c(df_tot, df_reg, df_res, NA_integer_),
    MS = c(ss_tot/df_tot, ms_reg, ms_res, NA_real_),
    F_value = c(NA_real_, NA_real_, NA_real_, F_val),
    p_value = c(NA_real_, NA_real_, NA_real_, p_val),
    row.names = c("Total corrected", "Regression", "Residual", "RESULT")
  )

  out
}
