#' Threshold Adjacency Matrices Based on Shuffled Network Quantiles
#'
#' Applies a cutoff to weighted adjacency matrices using a percentile
#' estimated from shuffled versions of the original expression matrices.
#' Supports inference methods \code{"GENIE3"}, \code{"GRNBoost2"},
#' and \code{"JRF"}.
#'
#' @param count_matrices A \linkS4class{MultiAssayExperiment} object
#'   containing expression data from multiple experiments or conditions.
#' @param weighted_adjm_list A \linkS4class{SummarizedExperiment} object
#'   containing weighted adjacency matrices (one per experiment) to threshold.
#' @param n Integer. Number of shuffled replicates generated per original
#'   expression matrix.
#' @param method Character string. One of \code{"GENIE3"}, \code{"GRNBoost2"},
#'   or \code{"JRF"}.
#' @param quantile_threshold Numeric. The quantile used to define the cutoff.
#'   Default is \code{0.99}.
#' @param weight_function Character string or function used to symmetrize
#'   adjacency matrices (\code{"mean"}, \code{"max"}, etc.).
#' @param nCores Integer. Number of CPU cores to use for
#'   parallelization. Default is the number of workers in the current
#'   \pkg{BiocParallel} backend. Note: JRF uses C implementation and
#'   does not use this parameter.
#' @param grnboost_modules Python modules needed for \code{GRNBoost2} if
#'   using reticulate.
#' @param debug Logical. If \code{TRUE}, prints detailed progress messages.
#'   Default is \code{FALSE}.
#'
#' @return A \linkS4class{SummarizedExperiment} object where each assay is a
#'   binary (thresholded) adjacency matrix corresponding to an input weighted
#'   matrix. Metadata includes cutoff values and method parameters.
#'
#' @details For each input expression matrix, \code{n} shuffled versions are
#'   generated by randomly permuting each gene’s expression across cells.
#'   Network inference is performed on the shuffled matrices, and a cutoff
#'   is determined as the specified quantile (\code{quantile_threshold}) of
#'   the resulting edge weights. The original weighted adjacency matrices
#'   are then thresholded using these estimated cutoffs.
#'
#'   Parallelization is handled via \pkg{BiocParallel}.
#'
#'   The methods are based on:
#'   \itemize{
#'     \item \strong{GENIE3}: Random Forest-based inference (Huynh-Thu et
#'       al., 2010).
#'     \item \strong{GRNBoost2}: Gradient boosting trees using arboreto
#'       (Moerman et al., 2019).
#'     \item \strong{JRF}: Joint Random Forests across multiple conditions
#'       (Petralia et al., 2015).
#'   }
#'
#' @importFrom BiocParallel bplapply MulticoreParam SerialParam bpworkers
#'   bpparam
#' @importFrom SummarizedExperiment assay
#' @export
#'
#' @examples
#' data(toy_counts)
#'
#'
#' # Infer networks (toy_counts is already a MultiAssayExperiment)
#' networks <- infer_networks(
#'     count_matrices_list = toy_counts,
#'     method = "GENIE3",
#'     nCores = 1
#' )
#' head(networks[[1]])
#'
#' # Generate adjacency matrices
#' wadj_se <- generate_adjacency(networks)
#' swadj_se <- symmetrize(wadj_se, weight_function = "mean")
#'
#' # Apply cutoff
#' binary_se <- cutoff_adjacency(
#'     count_matrices = toy_counts,
#'     weighted_adjm_list = swadj_se,
#'     n = 1,
#'     method = "GENIE3",
#'     quantile_threshold = 0.95,
#'     nCores = 1,
#'     debug = TRUE
#' )
#' head(binary_se[[1]])
cutoff_adjacency <- function(
    count_matrices,
    weighted_adjm_list,
    n,
    method = c("GENIE3", "GRNBoost2", "JRF"),
    quantile_threshold = 0.99,
    weight_function = "mean",
    nCores = 1,
    grnboost_modules = NULL,
    debug = FALSE) {
    method <- match.arg(method)
    weight_function <- match.fun(weight_function)

    if (!inherits(count_matrices, "MultiAssayExperiment")) {
        stop("count_matrices must be a MultiAssayExperiment object")
    }

    if (!inherits(weighted_adjm_list, "SummarizedExperiment")) {
        stop("weighted_adjm_list must be a SummarizedExperiment object")
    }

    count_matrices <- .extract_from_mae(count_matrices)
    weighted_adjm_list <- .extract_networks_from_se(weighted_adjm_list)

    if (length(count_matrices) != length(weighted_adjm_list)) {
        stop("Length of count_matrices must match weighted_adjm_list.")
    }

    count_matrices <- .convert_counts_list(count_matrices)

    if (method == "JRF") {
        # JRF-SPECIFIC LOGIC: Joint null distribution approach
        if (debug) {
            message("[JRF] for ", n, " shuffle replicates")
        }

        # For JRF, we need to process all matrices together for each shuffle
        results <- lapply(seq_len(n), function(shuffle_idx) {
            if (debug) {
                message("[JRF] Processing joint shuffle", shuffle_idx, "/", n)
            }

            # Get condition-specific cutoffs from joint null distribution
            cutoffs_vector <- .run_jrf_on_shuffled_joint(
                count_matrices,
                method,
                weight_function,
                quantile_threshold
            )

            # Return cutoffs for each condition
            lapply(seq_along(cutoffs_vector), function(cond_idx) {
                list(matrix_idx = cond_idx, q_value = cutoffs_vector[cond_idx])
            })
        })

        # Flatten the nested results
        results <- unlist(results, recursive = FALSE)
        cutoffs <- .aggregate_cutoffs(results, length(count_matrices))
    } else {
        # ORIGINAL LOGIC: Individual matrices (GENIE3/GRNBoost2)
        job_list <- expand.grid(
            matrix_idx  = seq_along(count_matrices),
            shuffle_idx = seq_len(n)
        )
        jobs <- lapply(
            seq_len(nrow(job_list)),
            function(i) {
                list(
                    matrix_idx  = job_list$matrix_idx[i],
                    shuffle_idx = job_list$shuffle_idx[i]
                )
            }
        )

        # Use parallel processing
        param_outer <- BiocParallel::MulticoreParam(workers = nCores)
        results <- BiocParallel::bplapply(
            jobs,
            function(job) {
                mat_idx <- job$matrix_idx
                mat <- count_matrices[[mat_idx]]
                q_value <- .run_network_on_shuffled(
                    mat,
                    method,
                    grnboost_modules,
                    weight_function,
                    quantile_threshold
                )
                list(matrix_idx = mat_idx, q_value = q_value)
            },
            BPPARAM = param_outer
        )

        cutoffs <- .aggregate_cutoffs(results, length(count_matrices))
    }
    binary_list <- .binarize_adjacency(
        weighted_adjm_list,
        cutoffs,
        method,
        debug
    )

    # Return as SummarizedExperiment with binary matrices
    if (is.null(names(binary_list))) {
        names(binary_list) <- paste0("network_", seq_along(binary_list))
    }

    build_network_se(
        networks = binary_list,
        networkData = S4Vectors::DataFrame(
            network = names(binary_list),
            n_edges = vapply(binary_list, function(x) sum(x > 0), numeric(1)),
            cutoff_used = vapply(cutoffs, mean, numeric(1)),
            row.names = names(binary_list)
        ),
        metadata = list(
            type = "binary",
            method = method,
            quantile_threshold = quantile_threshold,
            n_shuffles = n
        )
    )
}
