#' Estimate threshold value via Monte-Carlo simulations.
#' 
#' @description
#' Estimates the threshold for the maximum absolute studentized residual in a
#' Gaussian linear model, conditional on the design matrix and 
#' using `n_sims` Monte-Carlo simulation for the quantile of order `quant`.
#' 
#' @details
#' Monte-Carlo simulations are parallelized using OpenMP when available.
#'   
#' @param x either a numeric variable or several numeric variables
#'   (explanatory variables) concatenated in a data frame. 
#'   **Note:** `devianLM` does not add an intercept automatically; 
#'   include a column of ones in `x` if an intercept is desired.
#' @param n_sims Integer. Optional value which is the number of Monte-Carlo simulations. Default is 50,000.
#' @param nthreads Integer. Optional value which is the number of threads to use. Default is
#'   `parallel::detectCores() - 1`.
#' @param quant Numeric. Order of the quantile of interest. Default is 0.95 (this corresponds to a  risk level of 0.05).
#' 
#' @return Numeric value. \item{threshold}{The quantile of order `quant` of the distribution of the maximum of the absolute values of the studentized residuals (depending on the design matrix) is computed via Monte-Carlo simulations (with n_sims simulations).}
#' 
#' @export
#' @useDynLib devianLM, .registration = TRUE
#' @importFrom Rcpp sourceCpp
#' @import parallel
get_devianlm_threshold <- function( x, n_sims = 50000, nthreads = detectCores() - 1, quant = 0.95 ) {
  sims <- devianlm_cpp(x, n_sims, nthreads)
  quantile(sims, probs = quant)
}

#' Identify outliers using devianLM method
#' 
#' @description 
#' This function determines whether the maximum of the absolute values
#' of the studentized residuals of a Gaussian regression is abnormally high.
#' Outliers are detected by comparing the absolute values of the studentized residuals to a
#' threshold (depending on the design matrix), which can be supplied or estimated
#' via `n_sims` Monte-Carlo simulations.
#' 
#' @param y Numeric. Response vector.
#' @param x either a numeric variable or several numeric variables
#'   (explanatory variables) concatenated in a data frame. `devianLM` does not 
#'   add an intercept automatically; include a column of ones in `x` 
#'   if an intercept is desired.
#' @param threshold Numeric or NULL. If NULL, the threshold value is computed using [get_devianlm_threshold()].
#' @param n_sims Integer. Optional value which is the number of Monte-Carlo simulations. Default is 50,000.
#' @param verbose Logical. If TRUE (default), informative messages are printed during execution (e.g., when ties are detected and handled).
#' @param nthreads Integer. Optional value which is the number of threads to use. Default is
#'   `parallel::detectCores() - 1`.
#' @param quant Numeric. Order of the quantile of interest. Default is 0.95 (this corresponds to a  risk level of 0.05).
#' @param ... Additional arguments passed to [get_devianlm_threshold()].
#' 
#' @details
#' When ties are present in `y`, a small random perturbation is added to avoid
#' numerical issues. The "Ties were detected in the data, 
#' they have been randomly broken" message is displayed when this occurs.
#' 
#' @return devianlm returns an object of class \emph{list} with the following components:
#' \describe{
#'   \item{reg_residuals}{Numeric vector. The studentized residuals from the linear model.}
#'   \item{outliers}{Integer vector. The indices (positions in the original data) of observations identified as outliers based on the threshold.}
#'   \item{threshold}{Numeric value. The cutoff applied to the absolute value of the studentized residuals to flag outliers. If not provided, it is estimated using \code{get_devianlm_threshold()}.}
#'   \item{is_outliers}{Integer vector. A binary vector (0 or 1) of the same length as \code{reg_residuals}, indicating whether each observation is considered an outlier (1) or not (0).}
#' }
#' @importFrom stats .lm.fit lm.influence quantile rnorm complete.cases
#' @import parallel
#' @examples
#' set.seed(123)
#' y <- salary$hourly_earnings_log
#' x <- cbind(1, salary$age, salary$educational_attainment, salary$children_number)
#' 
#' test_salary <- devianlm_stats(y, x, n_sims = 100, quant = 0.95)
#' 
#' plot(test_salary$reg_residuals,
#'   pch = 16, cex = .8,
#'   ylim = c(-1 * max(abs(test_salary$reg_residuals)), max(abs(test_salary$reg_residuals))),
#'   xlab = "", ylab = "Studentized residuals",
#'   col = ifelse(test_salary$is_outliers, "red", "black"))
#' 
#' # Add the thresholds lines:
#' abline(h = c(-test_salary$threshold, test_salary$threshold), col = "chartreuse2", lwd = 2)
#'  
#' @export
devianlm_stats <- function(
    y,
    x,
    threshold = NULL,
    n_sims = 50000,
    verbose = TRUE,
    nthreads = detectCores() - 1,
    quant = 0.95,    
    ... 
) {
  
  if (length(y) != NROW(x)) stop("y and x must have compatible lengths")

  # dealing with NA
  complete_cases <- complete.cases(y, x)
  y_clean <- y[complete_cases]
  x_clean <- x[complete_cases, , drop = FALSE]

  if (is.null(threshold)) {
    threshold <- get_devianlm_threshold( x = x_clean, n_sims = n_sims, nthreads = nthreads, quant = quant )
  }
  
  # add noise if ties are detected
  y_mod <- y_clean
  if (any(duplicated(y_clean))) {
    v <- min(y_clean, na.rm = T) / 1e6
    if (v < 1e-16) v <- .Machine$double.eps
    y_mod <- y_clean + ifelse(duplicated(y_clean), rnorm(length(y_clean), mean = 0, sd = v), 0)
    if (verbose) {
      message("Ties were detected in the data; they have been randomly broken.")
    }
  }
  
  reg <- .lm.fit(x_clean, y_mod)
  reg$qr <- reg
  inf <- lm.influence(reg, do.coef = FALSE)
  reg_residuals <- inf$wt.res / (inf$sigma * sqrt(1 - cbind(inf$hat)))
  
  outliers <- which(abs(reg_residuals) > threshold)
  is_outliers <- integer(length(reg_residuals))
  is_outliers[outliers] <- 1
  
  res <- list(
    reg_residuals = reg_residuals,
    outliers = outliers,
    threshold = threshold,
    is_outliers = is_outliers
  )
  
  class(res) <- "devianlm"
  
  res
  
}
