#' @include internal.R
#'
#' @title Register an atomic objective (internal)
#' @name register_atomic_objective_internal
#'
#' @description
#' Internal helper used by objective setter functions to define the active
#' single-objective configuration of a \code{Problem} object and, optionally,
#' register that objective as an atomic objective for multi-objective workflows.
#'
#' @details
#' In \code{multiscape}, an \emph{atomic objective} is a fully specified
#' objective definition that can later be reused by a multi-objective method
#' such as a weighted-sum formulation, an \eqn{\epsilon}-constraint method,
#' AUGMECON, or other objective-orchestration procedures.
#'
#' Each atomic objective is identified by:
#' \itemize{
#'   \item a stable internal identifier \code{objective_id},
#'   \item a solver-facing model label \code{model_type},
#'   \item a list of objective-specific arguments stored in
#'   \code{objective_args},
#'   \item an optimization sense, either \code{"min"} or \code{"max"},
#'   \item and, optionally, a user-facing identifier \code{alias}.
#' }
#'
#' If \code{alias} is not \code{NULL}, the objective is stored in
#' \code{x$data$objectives[[alias]]}. This makes it possible to refer to the
#' same objective later by a stable user-facing name. For example, a user may
#' register objectives under aliases such as \code{"cost"},
#' \code{"benefit"}, or \code{"frag"} and then pass those aliases to a
#' multi-objective method.
#'
#' If \code{alias} is \code{NULL}, no atomic-objective entry is created in
#' \code{x$data$objectives}. In that case, the calling function still defines
#' the currently active single-objective configuration through
#' \code{x$data$model_args}, but no reusable multi-objective registration is
#' created.
#'
#' Thus, this helper supports two complementary modes:
#' \itemize{
#'   \item \strong{single-objective mode}: only the active objective is stored,
#'   \item \strong{multi-objective-ready mode}: the active objective is stored
#'   and also registered under an alias for later reuse.
#' }
#'
#' Conceptually, if an objective function is denoted by \eqn{f(x)}, this helper
#' does not itself define the mathematical form of \eqn{f}; rather, it stores
#' the metadata required so that downstream code can reconstruct the correct
#' objective expression, its direction of optimization, and its user-visible
#' identity.
#'
#' @param x A \code{Problem} object.
#' @param alias Optional character scalar used to register the objective as an
#'   atomic objective. If \code{NULL}, no registration entry is created.
#' @param objective_id Character string giving the stable internal identifier of
#'   the objective, for example \code{"min_cost"} or \code{"max_benefit"}.
#' @param model_type Character string giving the model-builder label associated
#'   with this objective, for example \code{"minimizeCosts"}.
#' @param objective_args A list of objective-specific arguments to be stored with
#'   the objective definition.
#' @param sense Character string giving the optimization direction. Must be
#'   either \code{"min"} or \code{"max"}.
#'
#' @return An updated \code{Problem} object.
#'
#' @keywords internal
NULL

#' @title Add objective: minimize cost
#'
#' @description
#' Define an objective that minimizes the total cost of the solution.
#'
#' Depending on the function arguments, the objective may include planning-unit
#' costs, action costs, or both. Action costs can optionally be restricted to a
#' subset of actions.
#'
#' @details
#' Use this function when the planning problem is framed primarily as a
#' cost-minimization problem, with costs arising from planning-unit selection,
#' action implementation, or both.
#'
#' Let \eqn{\mathcal{I}} be the set of planning units and let
#' \eqn{\mathcal{D} \subseteq \mathcal{I} \times \mathcal{A}} denote the set of
#' feasible planning unit--action decisions.
#'
#' Let:
#' \itemize{
#'   \item \eqn{w_i \in \{0,1\}} denote whether planning unit \eqn{i} is
#'   selected,
#'   \item \eqn{x_{ia} \in \{0,1\}} denote whether action \eqn{a} is selected
#'   in planning unit \eqn{i},
#'   \item \eqn{c_i^{PU} \ge 0} denote the planning-unit cost of unit \eqn{i},
#'   \item \eqn{c_{ia}^{A} \ge 0} denote the cost of selecting action
#'   \eqn{a} in planning unit \eqn{i}.
#' }
#'
#' The most general form of this objective is:
#'
#' \deqn{
#' \min \left(
#' \sum_{i \in \mathcal{I}} c_i^{PU} w_i
#' +
#' \sum_{(i,a) \in \mathcal{D}^{\star}} c_{ia}^{A} x_{ia}
#' \right),
#' }
#'
#' where \eqn{\mathcal{D}^{\star}} denotes the subset of feasible decisions
#' whose action contributes to the action-cost term.
#'
#' If \code{include_pu_cost = FALSE}, the planning-unit cost term is omitted.
#'
#' If \code{include_action_cost = FALSE}, the action-cost term is omitted.
#'
#' If \code{actions = NULL}, all feasible actions contribute to the action-cost
#' term. If \code{actions} is supplied, only the selected subset contributes to
#' that term. Planning-unit costs are never subset by \code{actions}; they are
#' always global whenever \code{include_pu_cost = TRUE}.
#'
#' @param x A \code{Problem} object.
#' @param include_pu_cost Logical. If \code{TRUE}, include planning-unit costs
#'   in the objective.
#' @param include_action_cost Logical. If \code{TRUE}, include action costs in
#'   the objective.
#' @param actions Optional subset of actions to include in the action-cost
#'   component. Values may match \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}. If \code{NULL}, all feasible actions are
#'   included in the action-cost term.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#'
#' p <- 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))
#'
#' p1 <- add_objective_min_cost(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_min_cost(
#'   p,
#'   include_pu_cost = FALSE,
#'   include_action_cost = TRUE
#' )
#' p2$data$model_args
#'
#' p3 <- add_objective_min_cost(
#'   p,
#'   actions = "restoration"
#' )
#' p3$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_max_profit}},
#' \code{\link{add_objective_max_net_profit}}
#'
#' @export
add_objective_min_cost <- function(
    x,
    include_pu_cost = TRUE,
    include_action_cost = TRUE,
    actions = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  args <- list(
    include_pu_cost = isTRUE(include_pu_cost),
    include_action_cost = isTRUE(include_action_cost),
    actions = if (is.null(action_subset)) NULL else as.character(action_subset$id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "minimizeCosts",
    objective_id = "min_cost",
    objective_args = args,
    sense = "min",
    alias = alias
  )
}

#' @title Add objective: maximize benefit
#'
#' @description
#' Define an objective that maximizes the total positive effects generated by
#' selected actions on selected features.
#'
#' This objective is based on the canonical effects table and uses only the
#' non-negative \code{benefit} component.
#'
#' @details
#' Use this function when positive ecological gains should be maximized
#' explicitly, without offsetting them against harmful effects.
#'
#' Let \eqn{b_{iaf} \ge 0} denote the stored benefit associated with planning
#' unit \eqn{i}, action \eqn{a}, and feature \eqn{f}. Since the effects table is
#' already expressed in canonical form, \eqn{b_{iaf}} represents the positive
#' part of the net effect associated with the corresponding selected action
#' decision.
#'
#' If no subsets are supplied, the objective can be written as:
#'
#' \deqn{
#' \max \sum_{(i,a,f) \in \mathcal{R}} b_{iaf} \, x_{ia},
#' }
#'
#' where \eqn{\mathcal{R}} denotes the set of stored benefit rows and
#' \eqn{x_{ia} \in \{0,1\}} indicates whether action \eqn{a} is selected in
#' planning unit \eqn{i}.
#'
#' If \code{actions} is provided, only rows whose action belongs to the selected
#' subset contribute to the objective.
#'
#' If \code{features} is provided, only rows whose feature belongs to the
#' selected subset contribute to the objective.
#'
#' More generally, letting \eqn{\mathcal{R}^{\star}} be the subset induced by
#' the selected actions and features, the objective is:
#'
#' \deqn{
#' \max \sum_{(i,a,f) \in \mathcal{R}^{\star}} b_{iaf} \, x_{ia}.
#' }
#'
#' This objective maximizes gains only. It does not subtract losses. If harmful
#' effects should also be accounted for, they must be handled separately through
#' additional objectives or constraints.
#'
#' @param x A \code{Problem} object.
#' @param actions Optional subset of actions to include in the objective. Values
#'   may match \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}. If \code{NULL}, all actions are included.
#' @param features Optional subset of features to include in the objective.
#'   Values may match \code{x$data$features$id} and, if present,
#'   \code{x$data$features$name}. If \code{NULL}, all features are included.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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)
#' )
#'
#' p <- 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)
#'
#' p1 <- add_objective_max_benefit(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_max_benefit(
#'   p,
#'   actions = "restoration"
#' )
#' p2$data$model_args
#'
#' p3 <- add_objective_max_benefit(
#'   p,
#'   features = 1
#' )
#' p3$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_min_loss}},
#' \code{\link{add_effects}}
#'
#' @export
add_objective_max_benefit <- function(
    x,
    actions = NULL,
    features = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  feature_subset <- NULL

  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  if (!is.null(features)) {
    feature_subset <- .pa_resolve_feature_subset(x, features)
  }

  args <- list(
    actions = if (is.null(action_subset)) NULL else as.integer(action_subset$internal_id),
    features = if (is.null(feature_subset)) NULL else as.integer(feature_subset$internal_id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "maximizeBenefits",
    objective_id = "max_benefit",
    objective_args = args,
    sense = "max",
    alias = alias
  )
}

#' @title Add objective: minimize loss
#'
#' @description
#' Define an objective that minimizes the total negative effects generated by
#' selected actions on selected features.
#'
#' This objective is based on the canonical effects table and uses only the
#' non-negative \code{loss} component.
#'
#' @details
#' Use this function when harmful ecological effects should be minimized
#' explicitly, without offsetting them against beneficial effects.
#'
#' Let \eqn{\ell_{iaf} \ge 0} denote the stored loss associated with planning
#' unit \eqn{i}, action \eqn{a}, and feature \eqn{f}.
#'
#' If no subsets are supplied, the objective can be written as:
#'
#' \deqn{
#' \min \sum_{(i,a,f) \in \mathcal{R}} \ell_{iaf} \, x_{ia}.
#' }
#'
#' where \eqn{\mathcal{R}} denotes the set of stored loss rows and
#' \eqn{x_{ia} \in \{0,1\}} indicates whether action \eqn{a} is selected in
#' planning unit \eqn{i}.
#'
#' If \code{actions} is provided, only rows whose action belongs to the selected
#' subset contribute to the objective.
#'
#' If \code{features} is provided, only rows whose feature belongs to the
#' selected subset contribute to the objective.
#'
#' More generally, letting \eqn{\mathcal{R}^{\star}} be the subset induced by
#' the selected actions and features, the objective is:
#'
#' \deqn{
#' \min \sum_{(i,a,f) \in \mathcal{R}^{\star}} \ell_{iaf} \, x_{ia}.
#' }
#'
#' This objective minimizes harmful effects only. It does not offset losses
#' against benefits unless benefits are handled elsewhere through additional
#' objectives or constraints.
#'
#' @param x A \code{Problem} object.
#' @param actions Optional subset of actions to include in the objective. Values
#'   may match \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}. If \code{NULL}, all actions are included.
#' @param features Optional subset of features to include in the objective.
#'   Values may match \code{x$data$features$id} and, if present,
#'   \code{x$data$features$name}. If \code{NULL}, all features are included.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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)
#' )
#'
#' p <- 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)
#'
#' p1 <- add_objective_min_loss(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_min_loss(
#'   p,
#'   actions = "restoration"
#' )
#' p2$data$model_args
#'
#' p3 <- add_objective_min_loss(
#'   p,
#'   features = 1
#' )
#' p3$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_max_benefit}},
#' \code{\link{add_effects}}
#'
#' @export
add_objective_min_loss <- function(
    x,
    actions = NULL,
    features = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  feature_subset <- NULL

  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  if (!is.null(features)) {
    feature_subset <- .pa_resolve_feature_subset(x, features)
  }

  args <- list(
    actions = if (is.null(action_subset)) NULL else as.integer(action_subset$internal_id),
    features = if (is.null(feature_subset)) NULL else as.integer(feature_subset$internal_id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "minimizeLosses",
    objective_id = "min_loss",
    objective_args = args,
    sense = "min",
    alias = alias
  )
}

#' @title Add objective: maximize profit
#'
#' @description
#' Define an objective that maximizes total profit from selected planning
#' unit--action decisions.
#'
#' @details
#' Use this function when the objective is to maximize gross economic return,
#' without subtracting planning-unit or action costs.
#'
#' Let \eqn{x_{ia} \in \{0,1\}} denote whether action \eqn{a} is selected in
#' planning unit \eqn{i}, and let \eqn{\pi_{ia}} denote the profit associated
#' with that decision, as taken from column \code{profit_col} in the stored
#' profit table.
#'
#' If all actions are included, the objective is:
#'
#' \deqn{
#' \max \sum_{(i,a) \in \mathcal{D}} \pi_{ia} x_{ia},
#' }
#'
#' where \eqn{\mathcal{D}} denotes the set of feasible planning unit--action
#' decisions.
#'
#' If \code{actions} is provided, only the selected subset contributes to the
#' objective. Letting \eqn{\mathcal{D}^{\star}} denote the feasible decisions
#' whose action belongs to the selected subset, the objective becomes:
#'
#' \deqn{
#' \max \sum_{(i,a) \in \mathcal{D}^{\star}} \pi_{ia} x_{ia}.
#' }
#'
#' This objective considers profit only. It does not subtract planning-unit
#' costs or action costs. For a net-profit formulation, use
#' \code{\link{add_objective_max_net_profit}}.
#'
#' @param x A \code{Problem} object.
#' @param profit_col Character string giving the profit column in the stored
#'   profit table.
#' @param actions Optional subset of actions to include. Values may match
#'   \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}. If \code{NULL}, all actions are included.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#' profit_df <- data.frame(
#'   pu = c(1, 2, 3, 4, 1, 2, 3, 4),
#'   action = c("conservation", "conservation", "conservation", "conservation",
#'              "restoration", "restoration", "restoration", "restoration"),
#'   profit = c(5, 4, 3, 2, 8, 7, 6, 5)
#' )
#'
#' p <- 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_profit(profit_df)
#'
#' p1 <- add_objective_max_profit(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_max_profit(
#'   p,
#'   actions = "restoration"
#' )
#' p2$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_min_cost}},
#' \code{\link{add_objective_max_net_profit}}
#'
#' @export
add_objective_max_profit <- function(
    x,
    profit_col = "profit",
    actions = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  args <- list(
    profit_col = as.character(profit_col)[1],
    actions = if (is.null(action_subset)) NULL else as.character(action_subset$id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "maximizeProfit",
    objective_id = "max_profit",
    objective_args = args,
    sense = "max",
    alias = alias
  )
}

#' @title Add objective: maximize net profit
#'
#' @description
#' Define an objective that maximizes net profit by combining profits with
#' optional planning-unit and action-cost penalties.
#'
#' @details
#' Use this function when decisions generate returns and the objective should
#' optimize the resulting net balance after subtracting selected cost
#' components.
#'
#' Let:
#' \itemize{
#'   \item \eqn{x_{ia} \in \{0,1\}} denote whether action \eqn{a} is selected
#'   in planning unit \eqn{i},
#'   \item \eqn{w_i \in \{0,1\}} denote whether planning unit \eqn{i} is
#'   selected,
#'   \item \eqn{\pi_{ia}} denote the profit associated with decision
#'   \eqn{(i,a)},
#'   \item \eqn{c_i^{PU} \ge 0} denote the planning-unit cost,
#'   \item \eqn{c_{ia}^{A} \ge 0} denote the action cost.
#' }
#'
#' In its most general form, the objective is:
#'
#' \deqn{
#' \max \left(
#' \sum_{(i,a) \in \mathcal{D}^{\star}} \pi_{ia} x_{ia}
#' -
#' \sum_{i \in \mathcal{I}} c_i^{PU} w_i
#' -
#' \sum_{(i,a) \in \mathcal{D}^{\star}} c_{ia}^{A} x_{ia}
#' \right),
#' }
#'
#' where \eqn{\mathcal{D}^{\star}} denotes the subset of feasible
#' planning unit--action decisions included in the objective.
#'
#' If \code{actions = NULL}, all feasible actions contribute to both the profit
#' term and the action-cost term.
#'
#' If \code{actions} is provided, the profit term and the action-cost term are
#' restricted to that subset. The planning-unit cost term, if included, remains
#' global.
#'
#' If \code{include_pu_cost = FALSE}, the planning-unit cost term is omitted.
#'
#' If \code{include_action_cost = FALSE}, the action-cost term is omitted.
#'
#' @param x A \code{Problem} object.
#' @param profit_col Character string giving the profit column in the stored
#'   profit table.
#' @param include_pu_cost Logical. If \code{TRUE}, subtract planning-unit costs.
#' @param include_action_cost Logical. If \code{TRUE}, subtract action costs.
#' @param actions Optional subset of actions to include in the profit and
#'   action-cost terms. Values may match \code{x$data$actions$id} and, if
#'   present, \code{x$data$actions$action_set}.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#' profit_df <- data.frame(
#'   pu = c(1, 2, 3, 4, 1, 2, 3, 4),
#'   action = c("conservation", "conservation", "conservation", "conservation",
#'              "restoration", "restoration", "restoration", "restoration"),
#'   profit = c(5, 4, 3, 2, 8, 7, 6, 5)
#' )
#'
#' p <- 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_profit(profit_df)
#'
#' p1 <- add_objective_max_net_profit(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_max_net_profit(
#'   p,
#'   include_pu_cost = FALSE,
#'   include_action_cost = TRUE
#' )
#' p2$data$model_args
#'
#' p3 <- add_objective_max_net_profit(
#'   p,
#'   actions = "restoration"
#' )
#' p3$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_max_profit}},
#' \code{\link{add_objective_min_cost}}
#'
#' @export
add_objective_max_net_profit <- function(
    x,
    profit_col = "profit",
    include_pu_cost = TRUE,
    include_action_cost = TRUE,
    actions = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  args <- list(
    profit_col = as.character(profit_col)[1],
    include_pu_cost = isTRUE(include_pu_cost),
    include_action_cost = isTRUE(include_action_cost),
    actions = if (is.null(action_subset)) NULL else as.character(action_subset$id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "maximizeNetProfit",
    objective_id = "max_net_profit",
    objective_args = args,
    sense = "max",
    alias = alias
  )
}

#' @title Add objective: minimize fragmentation
#'
#' @description
#' Define an objective that minimizes planning-unit fragmentation over a stored
#' spatial relation.
#'
#' This objective acts on the planning-unit selection pattern through the binary
#' planning-unit variables \eqn{w_i}. It is therefore appropriate when spatial
#' cohesion is to be encouraged at the level of the selected planning-unit set
#' as a whole.
#'
#' @details
#' Use this function when spatial cohesion should be encouraged at the level of
#' the selected planning-unit set as a whole.
#'
#' Let \eqn{\mathcal{I}} denote the set of planning units and let
#' \eqn{w_i \in \{0,1\}} indicate whether planning unit \eqn{i \in \mathcal{I}}
#' is selected.
#'
#' Let the chosen spatial relation define a set of weighted pairs with weights
#' \eqn{\omega_{ij} \ge 0}. These relation weights are interpreted by the model
#' builder after scaling by \eqn{\lambda =} \code{weight_multiplier}.
#'
#' The internal preparation step constructs one auxiliary variable
#' \eqn{y_{ij} \in [0,1]} for each unique non-diagonal undirected edge
#' \eqn{(i,j)} with \eqn{i < j}. The intended semantics is:
#' \deqn{
#' y_{ij} = w_i \land w_j.
#' }
#'
#' This is enforced by the standard linearization:
#' \deqn{
#' y_{ij} \le w_i,
#' }
#' \deqn{
#' y_{ij} \le w_j,
#' }
#' \deqn{
#' y_{ij} \ge w_i + w_j - 1.
#' }
#'
#' Thus, \eqn{y_{ij}=1} if and only if both planning units \eqn{i} and
#' \eqn{j} are selected, and \eqn{y_{ij}=0} otherwise.
#'
#' The exact objective coefficients are assembled later by the model builder
#' from:
#' \itemize{
#'   \item the planning-unit variables \eqn{w_i},
#'   \item the edge-conjunction variables \eqn{y_{ij}},
#'   \item the stored relation weights \eqn{\omega_{ij}},
#'   \item and the multiplier \eqn{\lambda}.
#' }
#'
#' Conceptually, the resulting objective is a boundary- or relation-based
#' compactness functional that penalizes exposed or weakly connected selected
#' patterns while rewarding adjacency among selected planning units.
#'
#' In the common case where \code{relation_name = "boundary"} and the relation
#' was built with \code{\link{add_spatial_boundary}}, the objective corresponds
#' to a boundary-length-style fragmentation penalty.
#'
#' Setting \code{weight_multiplier = 0} removes the contribution of the spatial
#' relation from the objective after scaling.
#'
#' This objective does not distinguish between different actions within the same
#' planning unit. If action-specific spatial cohesion is required, use
#' \code{\link{add_objective_min_fragmentation_action}} instead.
#'
#' @param x A \code{Problem} object.
#' @param relation_name Character string giving the name of the spatial relation
#'   to use. The relation must already exist in
#'   \code{x$data$spatial_relations}.
#' @param weight_multiplier Numeric scalar greater than or equal to zero. Global
#'   multiplier applied to the relation weights when the objective is built.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#'
#' bound_df <- data.frame(
#'   id1 = c(1, 1, 2, 1, 2, 3, 4),
#'   id2 = c(1, 2, 2, 3, 4, 4, 4),
#'   boundary = c(4, 1, 4, 1, 1, 1, 4)
#' )
#'
#' p <- 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))
#'
#' p <- add_spatial_boundary(
#'   x = p,
#'   boundary = bound_df,
#'   name = "boundary",
#'   include_self = TRUE,
#'   edge_factor = 1
#' )
#'
#' p <- add_objective_min_fragmentation_pu(
#'   p,
#'   relation_name = "boundary"
#' )
#'
#' p$data$model_args
#'
#' @seealso
#' \code{\link{add_spatial_boundary}},
#' \code{\link{add_spatial_relations}},
#' \code{\link{add_objective_min_fragmentation_action}}
#'
#' @export
add_objective_min_fragmentation_pu <- function(
    x,
    relation_name = "boundary",
    weight_multiplier = 1,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  relation_name <- as.character(relation_name)[1]
  weight_multiplier <- as.numeric(weight_multiplier)[1]

  if (!is.finite(weight_multiplier) || weight_multiplier < 0) {
    stop("weight_multiplier must be a finite number >= 0.", call. = FALSE)
  }

  rels <- x$data$spatial_relations
  if (is.null(rels) || !is.list(rels) || is.null(rels[[relation_name]])) {
    stop(
      "Spatial relation '", relation_name, "' not found in x$data$spatial_relations. ",
      "Add it first.",
      call. = FALSE
    )
  }

  args <- list(
    relation_name = relation_name,
    weight_multiplier = weight_multiplier
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "minimizeFragmentation",
    objective_id = "min_fragmentation",
    objective_args = args,
    sense = "min",
    alias = alias
  )
}

#' @title Add objective: minimize action fragmentation
#'
#' @description
#' Define an objective that minimizes fragmentation at the action level over a
#' stored spatial relation.
#'
#' Unlike \code{\link{add_objective_min_fragmentation_pu}}, which acts on the
#' selected planning-unit set through \eqn{w_i}, this objective acts on the
#' spatial arrangement of individual action decisions through the action
#' variables \eqn{x_{ia}}.
#'
#' @details
#' Use this function when spatial cohesion should be encouraged separately for
#' each selected action pattern.
#'
#' Let \eqn{\mathcal{I}} denote the set of planning units and let
#' \eqn{\mathcal{A}} denote the set of actions.
#'
#' Let \eqn{x_{ia} \in \{0,1\}} indicate whether action \eqn{a \in \mathcal{A}}
#' is selected in planning unit \eqn{i \in \mathcal{I}}.
#'
#' Let the chosen spatial relation define weighted pairs with
#' weights \eqn{\omega_{ij} \ge 0}, and let
#' \eqn{\lambda =} \code{weight_multiplier} be the global scaling factor applied
#' to these weights.
#'
#' If \code{actions} is supplied, only the selected subset
#' \eqn{\mathcal{A}^{\star} \subseteq \mathcal{A}} contributes to the final
#' objective. If \code{actions = NULL}, all actions are included.
#'
#' The internal preparation step constructs one auxiliary variable
#' \eqn{b_{ija} \in [0,1]} for each unique non-diagonal undirected edge
#' \eqn{(i,j)} with \eqn{i < j} and for each action \eqn{a}. The intended
#' semantics is:
#' \deqn{
#' b_{ija} = x_{ia} \land x_{ja}.
#' }
#'
#' Whenever both decision variables \eqn{x_{ia}} and \eqn{x_{ja}} exist in the
#' model, this conjunction is enforced by the linearization:
#' \deqn{
#' b_{ija} \le x_{ia},
#' }
#' \deqn{
#' b_{ija} \le x_{ja},
#' }
#' \deqn{
#' b_{ija} \ge x_{ia} + x_{ja} - 1.
#' }
#'
#' If one of the two action variables does not exist because the corresponding
#' \code{(pu, action)} pair is not feasible, the auxiliary variable is forced to
#' zero.
#'
#' Therefore, \eqn{b_{ija}=1} if and only if action \eqn{a} is selected in both
#' adjacent planning units \eqn{i} and \eqn{j}; otherwise \eqn{b_{ija}=0}.
#'
#' The exact objective coefficients are assembled later by the model builder
#' from:
#' \itemize{
#'   \item the action decision variables \eqn{x_{ia}},
#'   \item the edge-conjunction variables \eqn{b_{ija}},
#'   \item the relation weights \eqn{\omega_{ij}},
#'   \item the multiplier \eqn{\lambda},
#'   \item and, if supplied, the action-specific weights.
#' }
#'
#' If action-specific weights are provided, let \eqn{\alpha_a \ge 0} denote the
#' weight associated with action \eqn{a}. Then the resulting objective can be
#' interpreted as an action-wise compactness or fragmentation functional of the
#' form:
#' \deqn{
#' \min \sum_{a \in \mathcal{A}^{\star}} \alpha_a \,
#' F_a(x_{\cdot a}, b_{\cdot\cdot a}; \lambda \omega),
#' }
#' where \eqn{F_a} is the fragmentation expression induced by the selected
#' relation and the internal coefficient construction for action \eqn{a}.
#'
#' In practical terms, this objective penalizes solutions in which the same
#' action is spatially scattered or broken into separate patches, while allowing
#' different actions to form different spatial patterns.
#'
#' This differs from planning-unit fragmentation:
#' \itemize{
#'   \item \code{add_objective_min_fragmentation_pu()} encourages cohesion of the
#'   union of selected planning units,
#'   \item \code{add_objective_min_fragmentation_action()} encourages cohesion of
#'   each selected action pattern separately.
#' }
#'
#' Setting \code{weight_multiplier = 0} removes the contribution of the spatial
#' relation from the objective after scaling.
#'
#' @param x A \code{Problem} object.
#' @param relation_name Character string giving the name of the spatial relation
#'   to use. The relation must already exist in
#'   \code{x$data$spatial_relations}.
#' @param weight_multiplier Numeric scalar greater than or equal to zero. Global
#'   multiplier applied to the relation weights when the objective is built.
#' @param action_weights Optional action weights. Either a named numeric vector
#'   with names equal to action ids, or a \code{data.frame} with columns
#'   \code{action} and \code{weight}. These weights scale the contribution of
#'   each action to the final objective.
#' @param actions Optional subset of actions to include. Values may match
#'   \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}. If \code{NULL}, all actions are included.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#'
#' bound_df <- data.frame(
#'   id1 = c(1, 1, 2, 1, 2, 3, 4),
#'   id2 = c(1, 2, 2, 3, 4, 4, 4),
#'   boundary = c(4, 1, 4, 1, 1, 1, 4)
#' )
#'
#' p <- 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))
#'
#' p <- add_spatial_boundary(
#'   x = p,
#'   boundary = bound_df,
#'   name = "boundary",
#'   include_self = TRUE,
#'   edge_factor = 1
#' )
#'
#' p <- add_objective_min_fragmentation_action(
#'   p,
#'   relation_name = "boundary",
#'   actions = "restoration",
#'   weight_multiplier = 1
#' )
#'
#' p$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_min_fragmentation_pu}},
#' \code{\link{add_spatial_boundary}},
#' \code{\link{add_spatial_relations}}
#'
#' @export
add_objective_min_fragmentation_action <- function(
    x,
    relation_name = "boundary",
    weight_multiplier = 1,
    action_weights = NULL,
    actions = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  action_subset <- NULL
  if (!is.null(actions)) {
    action_subset <- .pa_resolve_action_subset(x, actions)
  }

  args <- list(
    relation_name = as.character(relation_name)[1],
    weight_multiplier = as.numeric(weight_multiplier)[1],
    action_weights = action_weights,
    actions = if (is.null(action_subset)) NULL else as.character(action_subset$id)
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "minimizeActionFragmentation",
    objective_id = "min_action_fragmentation",
    objective_args = args,
    sense = "min",
    alias = alias
  )
}

#' @title Add objective: minimize intervention impact
#'
#' @description
#' Define an objective that minimizes the impact associated with selecting
#' planning units for intervention.
#'
#' This objective uses planning-unit selection variables rather than summing the
#' same impact repeatedly over multiple actions. As a result, each planning unit
#' contributes at most once to the objective, regardless of how many feasible
#' actions exist in that unit.
#'
#' @details
#' Use this function when intervention itself has a baseline ecological, social,
#' or operational burden that should be minimized independently of the detailed
#' effects of particular actions.
#'
#' Let \eqn{w_i \in \{0,1\}} denote whether planning unit \eqn{i} is selected
#' for intervention. Let \eqn{q_{if}} denote the impact amount associated with
#' planning unit \eqn{i} and feature \eqn{f}, taken from column
#' \code{impact_col} in the feature-distribution table.
#'
#' If all selected features are included, the objective can be interpreted as:
#'
#' \deqn{
#' \min \sum_{i \in \mathcal{I}} \left( \sum_{f \in \mathcal{F}^{\star}} q_{if}
#' \right) w_i,
#' }
#'
#' where \eqn{\mathcal{F}^{\star}} denotes the selected subset of features.
#'
#' Thus, the coefficient attached to \eqn{w_i} is the aggregated impact of the
#' selected features in planning unit \eqn{i}.
#'
#' The role of \code{actions} in this objective is not to make impact additive
#' over actions, but to restrict the notion of intervention to planning units
#' that are relevant for the selected subset of actions in downstream model
#' construction. Even when multiple feasible actions exist in a planning unit,
#' the planning unit contributes at most once through \eqn{w_i}.
#'
#' This objective is useful when intervention itself has a baseline ecological,
#' social, or operational impact that should be minimized independently of the
#' detailed gain or loss generated by particular actions.
#'
#' @param x A \code{Problem} object.
#' @param impact_col Character string giving the column in the
#'   feature-distribution table that contains the per-\code{(pu, feature)}
#'   impact amount. The default is \code{"amount"}.
#' @param features Optional subset of features to include. Values may match
#'   \code{x$data$features$id} and, if present, \code{x$data$features$name}.
#' @param actions Optional subset of actions used to define the intervention
#'   context. Values may match \code{x$data$actions$id} and, if present,
#'   \code{x$data$actions$action_set}.
#' @param alias Optional identifier used to register this objective for
#'   multi-objective workflows.
#'
#' @return An updated \code{Problem} object.
#'
#' @examples
#' 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")
#' )
#'
#' p <- 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))
#'
#' p1 <- add_objective_min_intervention_impact(p)
#' p1$data$model_args
#'
#' p2 <- add_objective_min_intervention_impact(
#'   p,
#'   features = 1
#' )
#' p2$data$model_args
#'
#' p3 <- add_objective_min_intervention_impact(
#'   p,
#'   actions = "restoration"
#' )
#' p3$data$model_args
#'
#' @seealso
#' \code{\link{add_objective_max_benefit}},
#' \code{\link{add_objective_min_loss}}
#'
#' @export
add_objective_min_intervention_impact <- function(
    x,
    impact_col = "amount",
    features = NULL,
    actions = NULL,
    alias = NULL
) {
  stopifnot(inherits(x, "Problem"))

  impact_col <- as.character(impact_col)[1]
  if (is.na(impact_col) || !nzchar(impact_col)) {
    stop("impact_col must be a non-empty string.", call. = FALSE)
  }

  if (is.null(x$data$dist_features) ||
      !inherits(x$data$dist_features, "data.frame") ||
      nrow(x$data$dist_features) == 0) {
    stop("x$data$dist_features is missing or empty.", call. = FALSE)
  }

  if (!(impact_col %in% names(x$data$dist_features))) {
    stop(
      "impact_col '", impact_col, "' was not found in x$data$dist_features.",
      call. = FALSE
    )
  }

  feat_subset <- .pa_resolve_feature_subset(x, features = features)
  act_subset  <- .pa_resolve_action_subset(x, subset = actions)

  args <- list(
    impact_col = impact_col,
    features = feat_subset$id,
    actions = act_subset$id
  )

  .pa_set_active_and_register_objective(
    x = x,
    model_type = "minimizeInterventionImpact",
    objective_id = "min_intervention_impact",
    objective_args = args,
    sense = "min",
    alias = alias
  )
}
