#' @title Set the weighted-sum multi-objective method
#'
#' @description
#' Configure a \code{Problem} object to be solved with a weighted-sum
#' multi-objective method.
#'
#' In the weighted-sum method, several registered atomic objectives are combined
#' into a single scalar objective using a weighted linear combination. This
#' function stores that configuration in \code{x$data$method} so that it can be
#' used later by \code{\link{solve}}.
#'
#' @details
#' Use this method when several registered objectives should be combined into a
#' single scalar optimization problem through explicit preference weights.
#'
#' \strong{General idea}
#'
#' Suppose that a set of atomic objectives has already been registered in the
#' problem under aliases \eqn{k \in \mathcal{K}}. Let \eqn{f_k(x)} denote the
#' scalar value of objective \eqn{k}, and let \eqn{w_k} denote its user-supplied
#' weight.
#'
#' The weighted-sum method combines them into a single scalar objective of the
#' form:
#'
#' \deqn{
#' \sum_{k \in \mathcal{K}} w_k \, f_k(x).
#' }
#'
#' In practice, the exact sign convention used internally depends on the sense
#' of each registered atomic objective, for example whether it is a
#' minimization-type or maximization-type objective. The solving layer is
#' therefore responsible for constructing a solver-ready scalar objective from
#' the stored objective specifications and the requested weights.
#'
#' \strong{Atomic objectives requirement}
#'
#' The weighted-sum method can only be used with atomic objectives that have
#' already been registered under aliases. These aliases are typically created by
#' calling objective setters with an \code{alias} argument, for example:
#' \preformatted{
#' x <- x |>
#'   add_objective_min_cost(alias = "cost") |>
#'   add_objective_min_fragmentation(alias = "frag")
#' }
#'
#' Internally, each atomic objective is stored in
#' \code{x$data$objectives[[alias]]} together with its metadata, such as:
#' \itemize{
#'   \item \code{objective_id},
#'   \item \code{model_type},
#'   \item \code{sense},
#'   \item \code{objective_args}.
#' }
#'
#' The \code{aliases} argument passed to this function selects which of those
#' registered atomic objectives are included in the weighted combination.
#'
#' \strong{Weight normalization}
#'
#' If \code{normalize_weights = TRUE}, the supplied weights are rescaled to sum
#' to 1:
#'
#' \deqn{
#' \tilde{w}_k = \frac{w_k}{\sum_{j \in \mathcal{K}} w_j}.
#' }
#'
#' This normalization does not change the optimizer's solution in a pure
#' weighted-sum formulation as long as all weights are multiplied by the same
#' positive constant, but it can improve interpretability and numerical
#' conditioning.
#'
#' If \code{normalize_weights = FALSE}, the supplied weights are stored exactly
#' as provided.
#'
#' \strong{Objective scaling}
#'
#' If \code{objective_scaling = TRUE}, the solving layer is expected to scale the
#' participating objectives before combining them. The purpose of scaling is to
#' reduce distortions caused by objectives being measured on very different
#' numerical ranges.
#'
#' Conceptually, if \eqn{R_k} denotes a scale or range associated with objective
#' \eqn{k}, then a scaled weighted sum may be interpreted as:
#'
#' \deqn{
#' \sum_{k \in \mathcal{K}} w_k \, \frac{f_k(x)}{R_k}.
#' }
#'
#' The exact scaling rule is implemented later in the solving layer. This
#' function only stores whether objective scaling has been requested.
#'
#' \strong{Mixed objective senses}
#'
#' Weighted sums are straightforward when all participating objectives have the
#' same optimization sense. When minimization and maximization objectives are
#' mixed, the solving layer must standardize them internally before building the
#' scalar objective.
#'
#' This function validates that the requested aliases exist, but it does not
#' itself resolve objective-sense compatibility. That logic is delegated to the
#' downstream solving layer.
#'
#' \strong{Theoretical limitation}
#'
#' The weighted-sum method typically recovers only \emph{supported} efficient
#' solutions, that is, solutions lying on the convex hull of the Pareto front in
#' objective space. In non-convex multi-objective problems, especially mixed
#' integer problems, some efficient solutions cannot be obtained by any weighted
#' combination. In such cases, methods such as
#' \code{\link{set_method_epsilon_constraint}} or
#' \code{\link{set_method_augmecon}} may be preferable.
#'
#' \strong{Stored configuration}
#'
#' This function stores the method definition in \code{x$data$method} with:
#' \itemize{
#'   \item \code{name = "weighted"},
#'   \item \code{aliases},
#'   \item \code{weights},
#'   \item \code{normalize_weights},
#'   \item \code{objective_scaling}.
#' }
#'
#' The actual scalarization is performed later by \code{\link{solve}}.
#'
#' @param x A \code{Problem} object.
#' @param aliases Character vector of objective aliases to combine. Each alias
#'   must correspond to a previously registered atomic objective.
#' @param weights Numeric vector of weights, with the same length and order as
#'   \code{aliases}. Weights must be finite. If \code{normalize_weights = TRUE},
#'   they are rescaled to sum to 1 before being stored.
#' @param normalize_weights Logical. If \code{TRUE}, normalize the supplied
#'   weights to sum to 1 before storing them.
#' @param objective_scaling Logical. If \code{TRUE}, request scaling of the
#'   participating objectives before weighted aggregation in the solving layer.
#'
#' @return The updated \code{Problem} object with the weighted-sum method
#'   configuration stored in \code{x$data$method}.
#'
#' @examples
#' # Small toy problem
#' pu_tbl <- data.frame(
#'   id = 1:4,
#'   cost = c(1, 2, 3, 4)
#' )
#'
#' feat_tbl <- data.frame(
#'   id = 1:2,
#'   name = c("feature_1", "feature_2")
#' )
#'
#' dist_feat_tbl <- data.frame(
#'   pu = c(1, 1, 2, 3, 4),
#'   feature = c(1, 2, 2, 1, 2),
#'   amount = c(5, 2, 3, 4, 1)
#' )
#'
#' actions_df <- data.frame(
#'   id = c("conservation", "restoration"),
#'   name = c("conservation", "restoration")
#' )
#'
#' effects_df <- data.frame(
#'   pu = c(1, 2, 3, 4, 1, 2, 3, 4),
#'   action = c("conservation", "conservation", "conservation", "conservation",
#'              "restoration", "restoration", "restoration", "restoration"),
#'   feature = c(1, 1, 1, 1, 2, 2, 2, 2),
#'   benefit = c(2, 1, 0, 1, 3, 0, 1, 2),
#'   loss = c(0, 0, 1, 0, 0, 1, 0, 0)
#' )
#'
#' x <- create_problem(
#'   pu = pu_tbl,
#'   features = feat_tbl,
#'   dist_features = dist_feat_tbl,
#'   cost = "cost"
#' ) |>
#'   add_actions(actions_df, cost = c(conservation = 1, restoration = 2)) |>
#'   add_effects(effects_df) |>
#'   add_objective_min_cost(alias = "cost") |>
#'   add_objective_max_benefit(alias = "benefit")
#'
#' x <- set_method_weighted_sum(
#'   x,
#'   aliases = c("cost", "benefit"),
#'   weights = c(0.4, 0.6),
#'   normalize_weights = FALSE
#' )
#'
#' x$data$method
#'
#' # Normalize weights before storing
#' x2 <- set_method_weighted_sum(
#'   x,
#'   aliases = c("cost", "benefit"),
#'   weights = c(2, 3),
#'   normalize_weights = TRUE
#' )
#'
#' x2$data$method
#'
#' # Request objective scaling
#' x3 <- set_method_weighted_sum(
#'   x,
#'   aliases = c("cost", "benefit"),
#'   weights = c(0.7, 0.3),
#'   objective_scaling = TRUE
#' )
#'
#' x3$data$method
#'
#' @seealso
#' \code{\link{set_method_epsilon_constraint}},
#' \code{\link{set_method_augmecon}},
#' \code{\link{solve}}
#'
#' @export
set_method_weighted_sum <- function(x,
                                aliases,
                                weights,
                                normalize_weights = FALSE,
                                objective_scaling = FALSE) {

  # ---- promote (handles both Problem and MOProblem)
  #x <- .pamo_as_mo(x)

  # ---- validate aliases
  if (!is.character(aliases) || length(aliases) == 0L || anyNA(aliases)) {
    stop("`aliases` must be a non-empty character vector without NA.", call. = FALSE)
  }
  aliases <- as.character(aliases)

  if (any(!nzchar(aliases))) {
    stop("`aliases` must not contain empty strings.", call. = FALSE)
  }
  if (anyDuplicated(aliases) != 0L) {
    dups <- unique(aliases[duplicated(aliases)])
    stop("`aliases` must not contain duplicates: ", paste(dups, collapse = ", "), call. = FALSE)
  }

  # ---- validate weights
  if (!is.numeric(weights) || length(weights) != length(aliases) || anyNA(weights)) {
    stop("`weights` must be a numeric vector, same length as `aliases`, without NA.", call. = FALSE)
  }
  weights <- as.numeric(weights)

  if (any(!is.finite(weights))) stop("`weights` must be finite.", call. = FALSE)
  #if (any(weights < 0)) stop("`weights` must be non-negative.", call. = FALSE)

  # ---- normalize_weights flag
  if (!is.logical(normalize_weights) || length(normalize_weights) != 1L || is.na(normalize_weights)) {
    stop("`normalize_weights` must be TRUE or FALSE.", call. = FALSE)
  }
  normalize_weights <- isTRUE(normalize_weights)

  # ---- objective_scaling flag
  if (!is.logical(objective_scaling) || length(objective_scaling) != 1L || is.na(objective_scaling)) {
    stop("`objective_scaling` must be TRUE or FALSE.", call. = FALSE)
  }
  objective_scaling <- isTRUE(objective_scaling)

  if (normalize_weights) {
    s <- sum(weights)
    if (!is.finite(s) || s <= 0) {
      stop("`normalize_weights = TRUE` requires sum(weights) > 0.", call. = FALSE)
    }
    weights <- weights / s
  }

  # ---- validate that objective aliases exist (reads x$base$data$objectives)
  # This gives you nicer errors and also future-proofs (e.g., checks objective_id/sense)
  .pamo_get_objective_specs(x, aliases)

  # ---- store method configuration
  x$data$method <- list(
    name = "weighted",
    aliases = aliases,
    weights = weights,
    normalize_weights = normalize_weights,
    objective_scaling = objective_scaling
  )

  x
}
