#' Automatically select distribution families to be used for ROC smoothing.
#'
#' Automatically selects the best fitting distribution families to be used when smoothing ROC curves via population probability density function estimation. It takes a raw `roc` object generated by the \ifelse{html}{\code{\link[pROC:roc]{roc()}}}{\code{roc()}} function of the ‘pROC’ package as input, fits up to four different candidate distribution families to both the case and control data, and then uses AIC to select and return the best fitting distribution types along with any associated start values. These returned parameters can then be directly passed to the \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{smooth()}} function of the ‘pROC’ package for generation of a smoothed ROC curve that approximates population-level diagnostic performance using the `"fitdistr"` method.
#'
#' Similar to \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{smooth()}}, all fits are internally performed using the \ifelse{html}{\code{\link[MASS:fitdistr]{fitdistr()}}}{\code{fitdistr()}} function of the ‘MASS’ package.  Method of moments is used to establish starting values for distribution types where they are explicitly needed. When using [find.distr()] in tandem with \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{smooth()}}, [get.fits()] can be used to retrieve the final fits ultimate used by \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{smooth()}} for generation of the smooth ROC curve. Furthermore, [sm.coords()] can be used to calculate population-level diagnostic performance and cutoff estimates from the smoothed curve.
#'
#' @param roc
#' A `roc` object generated by the \ifelse{html}{\code{\link[pROC:roc]{roc()}}}{\code{roc()}} function.
#'
#' @param candidates
#' A character vector specifying the candidate distribution families to be tested. Options include ` “normal” `, ` “lognormal” `, ` “gamma” `, and ` “weibull” `. If ` NULL `, all four families will be tested by default.
#'
#' @param search.mode
#' Character string specifying what search mode to use to select the best fitting distribution families. One of `"all_pairs"` or `"per_group"`. If `"all_pairs"`, all possible case and control distribution family combinations are tested jointly, and the pair that minimizes the combined AIC across both groups is selected and returned. If `"per_group"`, distribution families are tested independently for the case and control groups, and the distribution families that respectively minimize AIC within each group are selected and returned. Default is `"all_pairs"`. If  `"all_pairs"`mode fails, `"per_group"` mode is used as a fallback with warning message.
#'
#' @param quiet
#' Specifies whether to silence messages generated during distribution selection. Default value is `TRUE`.
#'
#' @return
#' A named list containing the parameters associated with the best fitting case and control distribution families. The returned list can be passed directly to \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{smooth()}} for generation of a smooth ROC curve using the `"fitdistr"` method, and has the following elements:
#'
#' \item{`density.controls`}{A character string specifying the distribution family selected for the control group.}
#' \item{`density.cases`}{A character string specifying the distribution family selected for the case group.}
#' \item{`start.controls`}{An optional named list of starting values for fitting the control distribution. Returned only for distribution families that require explicit starting values.}
#' \item{`start.cases`}{An optional named list of starting values for fitting the case distribution. Returned only for distribution families that require explicit starting values.}
#'
#' Diagnostic information associated with model selection process is attached attributes, including all calculated AIC values and the final search mode.
#'
#' @seealso
#' \ifelse{html}{\link[pROC]{pROC-package}}{pROC-package} \cr
#' \ifelse{html}{\code{\link[pROC:roc]{roc()}}}{\code{pROC::roc()}} \cr
#' \ifelse{html}{\code{\link[pROC:smooth]{smooth()}}}{\code{pROC::smooth()}} \cr
#' \ifelse{html}{\code{\link[MASS:fitdistr]{fitdistr()}}}{\code{MASS::fitdistr()}} \cr
#' [rocTools-package] \cr
#' [get.fits()] \cr
#' [sm.coords()] \cr
#' \code{\link{blood_data}}
#'
#' @examples
#' # EXAMPLE USING LONG CALLS
#'
#' # load packages and example blood biomarker data
#'
#' library(pROC)
#' library(rocTools)
#' data(blood_data)
#'
#' # build raw ROC curve
#'
#' raw_roc <- roc(
#'   blood_data,
#'   response = "Diagnosis",
#'   predictor = "NfL"
#' )
#'
#' # find best fitting case and control distribution
#' # families to use for ROC smoothing
#'
#' best_fits <- find.distr(
#'   raw_roc,
#'   candidates = c("normal", "gamma", "lognormal", "weibull"),
#'   search.mode = "all_pairs",
#'   quiet = FALSE
#' )
#'
#' # use best fitting families to build smooth ROC curve
#'
#' smooth_roc <- smooth(
#'   raw_roc,
#'   method = "fitdistr",
#'   density.controls = best_fits$density.controls,
#'   density.cases = best_fits$density.cases,
#'   start.controls = best_fits$start.controls,
#'   start.cases = best_fits$start.cases
#' )
#'
#' # EXAMPLE USING COMPACT CALLS
#'
#' # load packages and example blood biomarker data
#'
#' library(pROC)
#' library(rocTools)
#' data(blood_data)
#'
#' # build raw ROC curve
#'
#' raw_roc <- roc(blood_data$Diagnosis, blood_data$NfL)
#'
#' # find best fitting case and controls distribution families
#' # and build smooth ROC curve in single call
#'
#' smooth_roc <- do.call(
#'   smooth,
#'   c(list(raw_roc, method = "fitdistr"), find.distr(raw_roc))
#' )
#'
#' @importFrom stats optimize pgamma plnorm pnorm pweibull qgamma qlnorm qnorm qweibull setNames uniroot
#' @importFrom MASS fitdistr
#' @import pROC
#' @export

find.distr<-function(
    roc,
    candidates=c("normal", "gamma", "lognormal", "weibull"),
    search.mode=c("all_pairs", "per_group"),
    quiet=FALSE
){

  # parse and check input
  # -----------------------------------------------------------

  # check if roc object

  roc_obj<-roc

  if(!inherits(roc_obj, "roc")){
    stop("Input must be a roc object generated by the 'pROC' package.")
  }

  allowed<-c("normal", "gamma", "lognormal", "weibull")
  if(!all(candidates%in%allowed)){
    stop(
      "Invalid candidate distribution(s). Allowed distributions include: ",
      paste(allowed, collapse=", ")
    )
  }

  candidates<-unique(candidates)
  search.mode<-match.arg(search.mode)

  controls<-roc_obj$controls
  cases<-roc_obj$cases

  # check for zero values

  if(any(controls<=0)||any(cases<=0)){

    candidates<-setdiff(candidates, "lognormal")

    if(quiet==FALSE){
      message(
        "Excluding lognormal: requires strictly positive values in both groups."
      )
    }
  }

  if(length(candidates)==0){
    stop("No candidate distributions remain after excluding lognormal.")
  }


  # shift x if any zero values are present

  make_positive<-function(x){

    if(any(x<=0)){

      xp<-x[x>0]

      if(length(xp)==0){
        shift<-1
      }else{
        shift<-min(xp)/1000
      }

      x<-x+shift
    }

    x
  }

  # build AIC helper
  # -----------------------------------------------------------

  safe_aic<-function(fit){

    if(inherits(fit, "try-error")){
      return(Inf)
    }

    ll<-fit$loglik

    if(!is.finite(ll)){
      return(Inf)
    }

    k<-length(fit$estimate)
    2*k-2*ll
  }

  # establish start values
  # -----------------------------------------------------------

  gamma_starts<-function(x){

    x<-make_positive(x)
    m<-mean(x)
    v<-var(x)

    if(!is.finite(m)||!is.finite(v)||m<=0||v<=0){
      return(list(shape=1, rate=1))
    }

    list(
      shape=max(m^2/v, 0.1),
      rate=max(m/v, 1e-4)
    )
  }

  weibull_mom_starts<-function(x){

    x<-make_positive(x)
    m<-mean(x)
    v<-var(x)

    if(!is.finite(m)||!is.finite(v)||m<=0||v<=0){
      return(list(shape=1.5, scale=max(m, 0.1)))
    }

    cv<-sqrt(v)/m

    moment_eq<-function(k){
      g1<-gamma(1+1/k)
      g2<-gamma(1+2/k)
      sqrt(g2/g1^2-1)-cv
    }

    root<-try(uniroot(moment_eq, c(0.1, 10)), silent=TRUE)

    if(inherits(root, "try-error")){
      k<-1.5
    }else{
      k<-root$root
    }

    lambda<-m/gamma(1+1/k)

    list(
      shape=max(k, 0.1),
      scale=max(lambda, 0.1)
    )
  }

  fit_one<-function(x, dist_name){

    x2<-make_positive(x)

    starts<-switch(
      dist_name,
      gamma=gamma_starts(x),
      weibull=weibull_mom_starts(x),
      NULL
    )

    suppressWarnings(
      try(
        MASS::fitdistr(x2, dist_name, start=starts),
        silent=TRUE
      )
    )
  }

  # calculate per-group AIC
  # -----------------------------------------------------------

  fits_ctrl<-lapply(candidates, fit_one, x=controls)
  fits_case<-lapply(candidates, fit_one, x=cases)

  AIC_ctrl<-setNames(sapply(fits_ctrl, safe_aic), candidates)
  AIC_case<-setNames(sapply(fits_case, safe_aic), candidates)

  if(all(!is.finite(AIC_ctrl))){
    stop("All candidate distributions failed to fit controls.")
  }

  if(all(!is.finite(AIC_case))){
    stop("All candidate distributions failed to fit cases.")
  }

  # select best fits
  # -----------------------------------------------------------

  if(search.mode=="per_group"){

    if(quiet==FALSE){
      message(
        "Per-group AIC selection across: ",
        paste(candidates, collapse=", ")
      )
    }

    best_ctrl<-candidates[which.min(AIC_ctrl)]
    best_case<-candidates[which.min(AIC_case)]

    AIC_pairs<-NA_real_

  }else{

    pairs<-expand.grid(
      ctrl=candidates,
      case=candidates,
      stringsAsFactors=FALSE
    )

    ic_vals<-vapply(
      seq_len(nrow(pairs)),
      function(i){
        AIC_ctrl[pairs$ctrl[i]]+AIC_case[pairs$case[i]]
      },
      numeric(1)
    )

    names(ic_vals)<-paste(pairs$ctrl, pairs$case, sep="_")
    best_i<-which.min(ic_vals)

    if(!length(best_i)||!is.finite(ic_vals[best_i])){

      if(quiet==FALSE){
        warning(
          "All-pairs AIC selection failed; falling back to per-group."
        )
      }

      best_ctrl<-candidates[which.min(AIC_ctrl)]
      best_case<-candidates[which.min(AIC_case)]
      AIC_pairs<-NA_real_

    }else{

      best_ctrl<-pairs$ctrl[best_i]
      best_case<-pairs$case[best_i]
      AIC_pairs<-ic_vals
    }

    if(quiet==FALSE){
      message(
        "All-pairs AIC across: ",
        paste(candidates, collapse=", ")
      )
      message(
        "Selected: controls = ", best_ctrl,
        ", cases = ", best_case
      )
    }
  }

  # build output
  # -----------------------------------------------------------

  out<-list(
    density.controls=best_ctrl,
    density.cases=best_case,
    start.controls=switch(
      best_ctrl,
      gamma=gamma_starts(controls),
      weibull=weibull_mom_starts(controls),
      NULL
    ),
    start.cases=switch(
      best_case,
      gamma=gamma_starts(cases),
      weibull=weibull_mom_starts(cases),
      NULL
    )
  )

  # attach attributes
  # -----------------------------------------------------------

  attr(out, "aic")<-list(
    controls=AIC_ctrl,
    cases=AIC_case,
    all_pairs=AIC_pairs
  )
  attr(out, "search.mode")<-search.mode

  return(out)
}
