#' Plate-level quality-control metrics for high-throughput screens
#'
#' Computes the standard set of assay quality metrics for a single plate from
#' per-control-type summary statistics: the Z-factor, Z-prime (Z'), the
#' signal-to-background ratio, the signal-to-noise ratio and the strictly
#' standardised mean difference (SSMD).
#'
#' @param df A data frame describing one plate with one row per control type. It
#'   must contain the columns `experiment_type`, `mean` and `sd`. The
#'   `experiment_type` column is expected to include the levels
#'   `"negative_ctrl"`, `"positive_cttl"` and `"sample"` (an optional
#'   `"edgewell"` level is ignored).
#'
#' @return A one-row data frame with columns `z_factor`, `z_prime`, `sb_value`,
#'   `sn_value` and `ssmd_value`.
#'
#' @details The metrics follow the usual HTS definitions, using the negative
#'   control and positive (kill) control populations:
#'   \deqn{Z\text{-}factor = 1 - \frac{3(sd_{neg} + sd_{sample})}{|m_{neg} - m_{sample}|}}
#'   \deqn{Z' = 1 - \frac{3(sd_{neg} + sd_{pos})}{|m_{neg} - m_{pos}|}}
#'   with signal-to-background \eqn{m_{neg}/m_{pos}}, signal-to-noise
#'   \eqn{(m_{pos} - m_{neg})/sd_{neg}} and
#'   \eqn{SSMD = (m_{pos} - m_{neg})/\sqrt{sd_{pos}^2 + sd_{neg}^2}}.
#'
#' @examples
#' plate <- data.frame(
#'   experiment_type = c("negative_ctrl", "positive_cttl", "sample"),
#'   mean = c(1.00, 0.10, 0.60),
#'   sd = c(0.05, 0.03, 0.20)
#' )
#' calculate_qc_metrics(plate)
#'
#' @seealso [flag_qc_plates()]
#' @export
calculate_qc_metrics <- function(df) {
  required <- c("experiment_type", "mean", "sd")
  missing <- setdiff(required, names(df))
  if (length(missing) > 0) {
    stop(
      "`df` is missing required column(s): ",
      paste(missing, collapse = ", "), ".",
      call. = FALSE
    )
  }

  pick <- function(type, col) {
    val <- df[[col]][df$experiment_type == type]
    if (length(val) == 0) NA_real_ else val[1]
  }

  m_negative <- pick("negative_ctrl", "mean")
  m_positive <- pick("positive_cttl", "mean")
  m_sample <- pick("sample", "mean")
  sd_negative <- pick("negative_ctrl", "sd")
  sd_positive <- pick("positive_cttl", "sd")
  sd_sample <- pick("sample", "sd")

  data.frame(
    z_factor = 1 - 3 * (sd_negative + sd_sample) / abs(m_negative - m_sample),
    z_prime = 1 - 3 * (sd_negative + sd_positive) / abs(m_negative - m_positive),
    sb_value = m_negative / m_positive,
    sn_value = (m_positive - m_negative) / sd_negative,
    ssmd_value = (m_positive - m_negative) / sqrt(sd_positive^2 + sd_negative^2)
  )
}

#' Flag plates that fail quality-control thresholds
#'
#' Adds pass/fail flags to a table of plate quality metrics such as the one
#' produced by [calculate_qc_metrics()], using conventional acceptance
#' thresholds.
#'
#' @param qc A data frame of plate quality metrics. Columns referenced by the
#'   active thresholds must be present (`z_prime`, `sb_value` and, if supplied,
#'   `cv_negative_ctrl`).
#' @param z_prime_min Minimum acceptable Z-prime. Plates below this are flagged
#'   `"Bad"`. Default `0.4`.
#' @param sb_min Minimum acceptable signal-to-background ratio. Default `3`.
#' @param cv_max Maximum acceptable coefficient of variation (percent) of the
#'   negative control. Only applied when a `cv_negative_ctrl` column is present.
#'   Default `10`.
#'
#' @return The input data frame with added character flag columns
#'   `z_primer_flag`, `sb_flag` and (when applicable) `cv_flag`, each taking the
#'   value `"Bad"` or `"Normal"`.
#'
#' @examples
#' qc <- data.frame(
#'   plateID = c("P01", "P02"),
#'   z_prime = c(0.62, 0.20),
#'   sb_value = c(5.1, 2.4),
#'   cv_negative_ctrl = c(6, 14)
#' )
#' flag_qc_plates(qc)
#'
#' @seealso [calculate_qc_metrics()]
#' @export
flag_qc_plates <- function(qc, z_prime_min = 0.4, sb_min = 3, cv_max = 10) {
  if ("z_prime" %in% names(qc)) {
    qc$z_primer_flag <- ifelse(qc$z_prime < z_prime_min, "Bad", "Normal")
  }
  if ("sb_value" %in% names(qc)) {
    qc$sb_flag <- ifelse(qc$sb_value < sb_min, "Bad", "Normal")
  }
  if ("cv_negative_ctrl" %in% names(qc)) {
    qc$cv_flag <- ifelse(qc$cv_negative_ctrl > cv_max, "Bad", "Normal")
  }
  qc
}
