#' @title Add absolute targets
#'
#' @description
#' Add feature-level absolute targets to a planning problem.
#'
#' These targets are stored in the problem object and later translated into
#' linear constraints when the optimization model is built. Absolute targets
#' are interpreted directly in the same units as the feature
#' contributions used by the model. Each call appends one or more target definitions to the problem. This makes
#' it possible to combine multiple target rules, including targets associated
#' with different action subsets.
#'
#' @details
#' Use this function when target requirements are naturally expressed in the
#' original units of the modelled feature contributions, rather than as
#' proportions of current baseline totals.
#'
#' Let \eqn{\mathcal{F}} denote the set of features. For each targeted feature
#' \eqn{f \in \mathcal{F}}, this function stores an absolute target threshold
#' \eqn{T_f \ge 0}.
#'
#' When the optimization model is built, each such target is interpreted as a
#' lower-bound constraint of the form:
#' \deqn{
#' \sum_{(i,a) \in \mathcal{D}_f^{\star}} c_{iaf} x_{ia} \ge T_f,
#' }
#' where:
#' \itemize{
#'   \item \eqn{i \in \mathcal{I}} indexes planning units,
#'   \item \eqn{a \in \mathcal{A}} indexes actions,
#'   \item \eqn{x_{ia}} indicates whether action \eqn{a} is selected in planning
#'   unit \eqn{i},
#'   \item \eqn{c_{iaf}} is the contribution of that action to feature
#'   \eqn{f},
#'   \item \eqn{\mathcal{D}_f^{\star}} is the subset of planning unit--action
#'   pairs allowed to count toward the target for feature \eqn{f}.
#' }
#'
#' In the absolute case, the stored target threshold is simply:
#' \deqn{
#' T_f = t_f,
#' }
#' where \eqn{t_f} is the user-supplied target value for feature \eqn{f}.
#'
#' The \code{actions} argument restricts which actions may contribute toward
#' achievement of the target, but it does not modify the value of \eqn{T_f}
#' itself.
#'
#' The \code{targets} argument is parsed by \code{.pa_parse_targets()} and may be
#' supplied in several equivalent forms, including:
#' \itemize{
#'   \item a single numeric value recycled to all selected features,
#'   \item a numeric vector aligned to \code{features},
#'   \item a named numeric vector where names identify features,
#'   \item a \code{data.frame} with \code{feature} and \code{target} columns.
#' }
#'
#' If \code{targets} does not explicitly identify features:
#' \itemize{
#'   \item if \code{features = NULL}, the target is applied to all features,
#'   \item if \code{features} is supplied, the target values are interpreted with
#'   respect to that feature set.
#' }
#'
#' Repeated calls append new target rules rather than replacing previous ones.
#' This allows cumulative target modelling, including multiple rules on the same
#' feature with different contributing action subsets.
#'
#' @param x A \code{Problem} object.
#' @param targets Target specification. This is interpreted as an absolute target
#'   value in the same units as the modelled feature contributions. It may be a
#'   scalar, vector, named vector, or \code{data.frame}. See Details.
#' @param features Optional feature specification indicating which features the
#'   supplied target values refer to when \code{targets} does not identify
#'   features explicitly. If \code{NULL}, all features are targeted.
#' @param actions Optional character vector indicating which actions count toward
#'   target achievement. Entries may match action ids, \code{action_set} labels,
#'   or both. If \code{NULL}, all actions count.
#' @param label Optional character string stored with the targets for reporting
#'   and bookkeeping.
#'
#' @return An updated \code{Problem} object with absolute targets appended to
#'   the stored target table.
#'
#' @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)
#' )
#'
#' p <- create_problem(
#'   pu = pu_tbl,
#'   features = feat_tbl,
#'   dist_features = dist_feat_tbl,
#'   cost = "cost"
#' ) |>
#'   add_actions(data.frame(id = "conservation", name = "conservation"), cost = 0)
#'
#' # Same absolute target for all features
#' p1 <- add_constraint_targets_absolute(p, 3)
#' p1$data$targets
#'
#' # Different targets by feature
#' p2 <- add_constraint_targets_absolute(
#'   p,
#'   c("1" = 4, "2" = 2)
#' )
#' p2$data$targets
#'
#' # Restrict which actions count toward target achievement
#' p3 <- add_constraint_targets_absolute(
#'   p,
#'   2,
#'   actions = "conservation"
#' )
#' p3$data$targets
#'
#' @seealso
#' \code{\link{add_constraint_targets_relative}}
#'
#' @export
add_constraint_targets_absolute <- function(x, targets,
                                 features = NULL,
                                 actions = NULL,
                                 label = NULL) {
  stopifnot(inherits(x, "Problem"))

  x <- .pa_clone_data(x)
  dt <- .pa_parse_targets(x, targets, features = features)

  actions_txt <- .pa_subset_to_string(actions)

  out <- data.frame(
    feature      = as.numeric(dt$feature),
    type         = "actions",
    sense        = "ge",
    target_unit  = "absolute",
    target_raw   = as.numeric(dt$target_raw),
    basis_total  = NA_real_,
    target_value = as.numeric(dt$target_raw),
    actions      = actions_txt,
    label        = if (is.null(label)) NA_character_ else as.character(label),
    created_at   = as.character(Sys.time()),
    stringsAsFactors = FALSE
  )

  out <- .pa_add_feature_labels(
    df = out,
    features_df = x$data$features,
    feature_col = "feature",
    internal_feature_col = "internal_feature",
    out_col = "feature_name"
  )

  x$data$meta$model_dirty <- TRUE
  .pa_store_targets(x, out)
}

#' @title Add relative targets
#'
#' @description
#' Add feature-level relative targets to a planning problem.
#'
#' These targets are stored in the problem object and later translated into
#' linear constraints when the optimization model is built. Relative targets are
#' supplied as proportions in \eqn{[0,1]} and are converted internally into
#' absolute thresholds using the current total amount of each
#' feature in the landscape.
#'
#' Each call appends one or more target definitions to the problem. This makes
#' it possible to combine multiple target rules, including targets associated
#' with different action subsets.
#'
#' @details
#' Use this function when target requirements are naturally expressed as
#' proportions of current baseline feature totals rather than in original
#' feature units.
#'
#' Let \eqn{\mathcal{F}} denote the set of features. For each targeted feature
#' \eqn{f \in \mathcal{F}}, let \eqn{B_f} denote the current baseline total
#' amount of that feature in the landscape, as computed by
#' \code{.pa_feature_totals()}.
#'
#' If the user supplies a relative target \eqn{r_f \in [0,1]}, then this
#' function converts it to an absolute threshold:
#' \deqn{
#' T_f = r_f \times B_f.
#' }
#'
#' The absolute threshold \eqn{T_f} is stored in \code{target_value}, while:
#' \itemize{
#'   \item the original user-supplied proportion \eqn{r_f} is stored in
#'   \code{target_raw},
#'   \item the baseline total \eqn{B_f} is stored in \code{basis_total}.
#' }
#'
#' When the optimization model is built, the resulting target is interpreted as:
#' \deqn{
#' \sum_{(i,a) \in \mathcal{D}_f^{\star}} c_{iaf} x_{ia} \ge T_f,
#' }
#' where:
#' \itemize{
#'   \item \eqn{i \in \mathcal{I}} indexes planning units,
#'   \item \eqn{a \in \mathcal{A}} indexes actions,
#'   \item \eqn{x_{ia}} indicates whether action \eqn{a} is selected in planning
#'   unit \eqn{i},
#'   \item \eqn{c_{iaf}} is the contribution of that action to feature
#'   \eqn{f},
#'   \item \eqn{\mathcal{D}_f^{\star}} is the subset of planning unit--action
#'   pairs allowed to count toward the target for feature \eqn{f}.
#' }
#'
#' The \code{actions} argument restricts which actions may contribute toward
#' target achievement, but it does not affect the baseline amount \eqn{B_f} used
#' to compute the threshold. In other words, relative targets are always scaled
#' against the current full landscape baseline.
#'
#' Therefore, \code{actions} changes who may satisfy the target, but not how the
#' threshold itself is scaled.
#'
#' The \code{targets} argument is parsed by \code{.pa_parse_targets()} and may be
#' supplied in several equivalent forms, including:
#' \itemize{
#'   \item a single numeric value recycled to all selected features,
#'   \item a numeric vector aligned to \code{features},
#'   \item a named numeric vector where names identify features,
#'   \item a \code{data.frame} with \code{feature} and \code{target} columns.
#' }
#'
#' If \code{targets} does not explicitly identify features:
#' \itemize{
#'   \item if \code{features = NULL}, the target is applied to all features,
#'   \item if \code{features} is supplied, the target values are interpreted with
#'   respect to that feature set.
#' }
#'
#' Relative targets must lie in \eqn{[0,1]}.
#'
#' Repeated calls append new target rules rather than replacing previous ones.
#' This allows cumulative target modelling, including multiple rules on the same
#' feature with different contributing action subsets.
#'
#' @param x A \code{Problem} object.
#' @param targets Target specification as proportions in \eqn{[0,1]}. It may be
#'   a scalar, vector, named vector, or \code{data.frame}. See Details.
#' @param features Optional feature specification indicating which features the
#'   supplied target values refer to when \code{targets} does not identify
#'   features explicitly. If \code{NULL}, all features are targeted.
#' @param actions Optional character vector indicating which actions count toward
#'   target achievement. Entries may match action ids, \code{action_set} labels,
#'   or both. If \code{NULL}, all actions count.
#' @param label Optional character string stored with the targets for reporting
#'   and bookkeeping.
#'
#' @return An updated \code{Problem} object with relative targets appended to
#'   the stored target table.
#'
#' @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)
#' )
#'
#' p <- create_problem(
#'   pu = pu_tbl,
#'   features = feat_tbl,
#'   dist_features = dist_feat_tbl,
#'   cost = "cost"
#' ) |>
#'   add_actions(data.frame(id = "conservation", name = "conservation"), cost = 0)
#'
#' # Require 30% of the baseline total for all features
#' p1 <- add_constraint_targets_relative(p, 0.3)
#' p1$data$targets
#'
#' # Require 20% for one selected feature
#' p2 <- add_constraint_targets_relative(
#'   p,
#'   0.2,
#'   features = 1
#' )
#' p2$data$targets
#'
#' # Restrict which actions count toward target achievement
#' p3 <- add_constraint_targets_relative(
#'   p,
#'   0.2,
#'   actions = "conservation"
#' )
#' p3$data$targets
#'
#' @seealso
#' \code{\link{add_constraint_targets_absolute}}
#'
#' @export
add_constraint_targets_relative <- function(x, targets,
                                 features = NULL,
                                 actions = NULL,
                                 label = NULL) {
  stopifnot(inherits(x, "Problem"))

  x <- .pa_clone_data(x)
  dt <- .pa_parse_targets(x, targets, features = features)

  rel <- as.numeric(dt$target_raw)
  if (any(rel < 0 | rel > 1, na.rm = TRUE)) {
    stop("Relative targets must be between 0 and 1.", call. = FALSE)
  }

  basis <- .pa_feature_totals(x)
  basis_v <- basis[as.character(dt$feature)]
  basis_v[is.na(basis_v)] <- 0
  abs_target <- rel * as.numeric(basis_v)

  actions_txt <- .pa_subset_to_string(actions)

  out <- data.frame(
    feature      = as.numeric(dt$feature),
    type         = "actions",
    sense        = "ge",
    target_unit  = "relative_baseline",
    target_raw   = rel,
    basis_total  = as.numeric(basis_v),
    target_value = as.numeric(abs_target),
    actions      = actions_txt,
    label        = if (is.null(label)) NA_character_ else as.character(label),
    created_at   = as.character(Sys.time()),
    stringsAsFactors = FALSE
  )

  out <- .pa_add_feature_labels(
    df = out,
    features_df = x$data$features,
    feature_col = "feature",
    internal_feature_col = "internal_feature",
    out_col = "feature_name"
  )

  x$data$meta$model_dirty <- TRUE
  .pa_store_targets(x, out)
}
