#' Best subset selection for generalised linear models via continuous
#' optimisation (COMBSS).
#' Reformulates the NP-hard discrete subset selection problem as a continuous
#' optimisation over the hypercube `[0, 1]^p`, solved via a Frank-Wolfe
#' homotopy algorithm. The inner ridge problem is solved with `glmnet`. For
#' each subset size `k = 1, ..., q`, COMBSS returns the selected feature set;
#' if validation data is supplied, the best `k` is chosen by validation MSE
#' (Gaussian) or accuracy (binomial / multinomial).
#'
#' @param x Numeric design matrix `(n, p)` without an intercept column.
#' @param y Response. Numeric for `family = "gaussian"`; `{0, 1}` (or two-level
#'   factor) for `family = "binomial"`; factor or integer in `{1, ..., C}` for
#'   `family = "multinomial"`.
#' @param family One of `"gaussian"` (alias: `"linear"`), `"binomial"`,
#'   `"multinomial"`.
#' @param q Maximum subset size. Defaults to `min(n, p)`.
#' @param x_val,y_val Optional validation data. When provided, the best `k`
#'   is selected by validation metric and `coef_` is returned for the best
#'   subset.
#' @param lam_ridge Ridge penalty for the inner solver. Default `0`.
#' @param Niter Number of homotopy iterations. Default `25`.
#' @param alpha Frank-Wolfe step size. Default `0.01`.
#' @param scale Column-normalise `x` before the algorithm. Default `TRUE`.
#' @param mandatory Optional integer vector of 1-indexed columns of `x` that
#'   must be included in every model.
#' @param verbose Print progress. Default `FALSE`.
#'
#' @return An object of class `"combss"`, a list with elements:
#'   - `family`: one of `"gaussian"`, `"binomial"`, `"multinomial"`
#'   - `subset_list`: list of length up to `q`; element `k` is an integer
#'     vector of selected column indices (1-indexed) for subset size `k`
#'   - `subset`: best subset (requires validation data; otherwise `NULL`)
#'   - `mse` / `accuracy`: validation metric for the best subset
#'     (requires validation data)
#'   - `lam_ridge`: ridge penalty used
#'   - `q`: maximum subset size evaluated
#'
#' @references
#' Moka, S., Liquet, B., Zhu, H. and Muller, S. (2024).
#' COMBSS: best subset selection via continuous optimization.
#' *Statistics and Computing*. \doi{10.1007/s11222-024-10387-8}
#'
#' Mathur, A., Liquet, B., Muller, S. and Moka, S. (2026).
#' Parsimonious Subset Selection for Generalized Linear Models with
#' Biomedical Applications. \href{https://arxiv.org/abs/2603.21952}{arXiv:2603.21952}.
#'
#' @examples
#' set.seed(1)
#' n <- 200; p <- 30
#' beta <- c(3, 2, 1.5, 1, 0.5, rep(0, p - 5))
#' x <- matrix(rnorm(n * p), n, p)
#' y <- as.numeric(x %*% beta + rnorm(n) * 0.5)
#' fit <- combss(x, y, family = "gaussian", q = 10)
#' fit$subset_list[1:5]
#'
#' @export
combss <- function(x, y,
                   family = c("gaussian", "binomial", "multinomial"),
                   q = NULL,
                   x_val = NULL, y_val = NULL,
                   lam_ridge = 0,
                   Niter = 25,
                   alpha = 0.01,
                   scale = TRUE,
                   mandatory = NULL,
                   verbose = FALSE) {

  family <- resolve_family(family)
  x <- as.matrix(x)
  n <- nrow(x); p <- ncol(x)
  if (is.null(q)) q <- min(n, p)

  if (family == "gaussian") {
    y <- as.numeric(y)
    solve_inner <- solve_inner_gaussian
  } else if (family == "binomial") {
    y <- as.numeric(y)
    solve_inner <- solve_inner_binomial
  } else {
    y <- as.factor(y)
    solve_inner <- solve_inner_multinomial
  }

  if (is.null(mandatory)) {
    mand_idx <- integer(0)
    sel_idx  <- seq_len(p)
  } else {
    mand_idx <- sort(as.integer(mandatory))
    sel_idx  <- setdiff(seq_len(p), mand_idx)
  }
  p_sel <- length(sel_idx)
  if (q > p_sel + length(mand_idx)) q <- p_sel + length(mand_idx)

  X_run <- if (scale) scale_design_matrix(x)$X_scaled else x
  cal <- calibrate_penalty_schedule(X_run[, sel_idx, drop = FALSE], n, family)

  if (verbose) {
    message(sprintf("Calibration: nu_max=%.4f | delta_conc=%.6f | delta_min=%.8f | delta_max=%.6f",
                    cal$nu_max, cal$delta_conc, cal$delta_min, cal$delta_max))
  }

  subset_list <- vector("list", q)
  for (k in seq_len(q)) {
    if (k <= length(mand_idx)) {
      subset_list[[k]] <- sort(mand_idx[seq_len(k)])
      next
    }
    k_relax <- k - length(mand_idx)
    t_init <- rep(k_relax / p_sel, p_sel)
    res <- fw_loop(t_init, k_relax, X_run, y,
                   cal$delta_min, cal$delta_max,
                   Niter, alpha, lam_ridge,
                   mand_idx, sel_idx, solve_inner)
    subset_list[[k]] <- res$selected
    if (verbose) {
      message(sprintf("  k = %2d  selected: %s",
                      k, paste(res$selected, collapse = ",")))
    }
  }

  out <- list(
    family       = family,
    subset_list  = subset_list,
    subset       = NULL,
    lam_ridge    = lam_ridge,
    q            = q,
    n            = n,
    p            = p,
    feature_names = colnames(x)
  )
  out[[if (family == "gaussian") "mse" else "accuracy"]] <- NULL

  if (!is.null(x_val) && !is.null(y_val)) {
    x_val <- as.matrix(x_val)
    y_val_raw <- y_val
    metric <- numeric(q)
    coefs  <- vector("list", q)
    for (k in seq_len(q)) {
      idx <- subset_list[[k]]
      pred <- refit_predict(x, y, x_val, idx, family)
      if (family == "gaussian") {
        metric[k] <- mean((as.numeric(y_val_raw) - pred)^2)
      } else if (family == "binomial") {
        metric[k] <- mean(pred == as.numeric(y_val_raw))
      } else {
        metric[k] <- mean(pred == as.character(y_val_raw))
      }
    }
    best_k <- if (family == "gaussian") which.min(metric) else which.max(metric)
    out$subset <- subset_list[[best_k]]
    if (family == "gaussian") {
      out$mse <- metric[best_k]
      out$mse_path <- metric
    } else {
      out$accuracy <- metric[best_k]
      out$accuracy_path <- metric
    }
    out$best_k <- best_k
  }

  class(out) <- "combss"
  out
}
