#' Make a price index aggregation structure
#'
#' Create a price index aggregation structure from a hierarchical
#' classification and aggregation weights that can be used to aggregate
#' elementary indexes.
#'
#' @aliases piar_aggregation_structure
#' @export
#'
#' @param x `[list]` A list of character vectors that give the codes/labels for
#'   each
#'   level of the classification, ordered so that moving down the list goes down
#'   the hierarchy. The last vector gives the elementary aggregates, which
#'   should have no duplicates. All vectors should be the same length, without
#'   `NA`s, and there should be no duplicates across different levels of
#'   `x`. Names for `x` are used as level names; otherwise, levels are named
#'   'level1', 'level2', ..., 'ea'.
#' @param weights `[numeric >= 0]` A numeric vector of aggregation weights for
#'   the elementary
#'   aggregates (i.e., the last vector in `x`), or something that can be coerced
#'   into one. The default is to give each elementary aggregate the same weight.
#'
#' @returns
#' A price index aggregation structure of class `piar_aggregation_structure`.
#' This is a list-S3 class with the following components.
#'
#' \item{child}{A nested list that gives the positions of the immediate
#' children for each node in each level of the aggregation structure above the
#' terminal nodes.}
#' \item{parent}{A list that gives the position of the
#' immediate parent for each node of the aggregation structure below the
#' initial nodes.}
#' \item{levels}{A named list of character vectors that give the levels of `x`.}
#' \item{weights}{A vector giving the weight for each elementary
#' aggregate.}
#'
#' @section Warning: The `aggregation_structure()` function does its best
#' to check its arguments, but there should be no expectation that the result
#' of `aggregation_structure()` will make any sense if `x` does not
#' represent a nested hierarchy.
#'
#' @seealso
#' [`aggregate()`][aggregate.piar_index] to aggregate price indexes made
#' with [`elementary_index()`][elementary_index].
#'
#' [expand_classification()] to make `x` from a character
#' representation of a hierarchical aggregation structure.
#'
#' [as_aggregation_structure()] to coerce tabular data into an
#' aggregation structure.
#'
#' [`as.data.frame()`][as.data.frame.piar_aggregation_structure] and
#' [`as.matrix()`][as.matrix.piar_aggregation_structure] to coerce an
#' aggregation structure into a tabular form.
#'
#' [`weights()`][weights.piar_aggregation_structure] to get the
#' weights for an aggregation structure.
#'
#' [`update()`][update.piar_aggregation_structure] for updating a
#' price index aggregation structure with an aggregated index.
#'
#' @examples
#' # A simple aggregation structure.
#' #            1
#' #      |-----+-----|
#' #      11          12
#' #  |---+---|       |
#' #  111     112     121
#' #  (1)     (3)     (4)
#' aggregation_weights <- data.frame(
#'   level1 = c("1", "1", "1"),
#'   level2 = c("11", "11", "12"),
#'   ea     = c("111", "112", "121"),
#'   weight = c(1, 3, 4)
#' )
#'
#' aggregation_structure(
#'   aggregation_weights[1:3],
#'   weights = aggregation_weights[[4]]
#' )
#'
#' # The aggregation structure can also be made by expanding the
#' # elementary aggregates.
#' with(
#'   aggregation_weights,
#'   aggregation_structure(expand_classification(ea), weight)
#' )
aggregation_structure <- function(x, weights = NULL) {
  x <- lapply(x, \(z) factor(z, unique(z)))
  len <- length(x)
  ea <- as.character(unlist(x[len], use.names = FALSE))
  if (length(ea) == 0L) {
    stop("cannot make an aggregation structure with no elementary aggregates")
  }
  if (anyNA(x, recursive = TRUE)) {
    stop("`x` cannot contain NAs")
  }

  if (is.null(weights)) {
    weights <- rep.int(1, length(ea))
  } else {
    weights <- as.numeric(weights)
    if (any(missing_weights(weights))) {
      warning("some elements of `weights` are NA or zero")
    }
  }

  # Basic argument checking to make sure inputs can make an
  # aggregation structure.
  if (any(lengths(x) != length(weights))) {
    stop("all arguments must be the same length")
  }
  if (anyDuplicated(ea)) {
    stop(
      "there are duplicated elementary aggregates; the last vector in `x` ",
      "should not have duplicates"
    )
  }
  if (anyDuplicated(unlist(lapply(x, unique), use.names = FALSE))) {
    stop(
      "there are duplicated nodes in the aggregation structure; the same ",
      "value cannot appear across multiple levels of `x`"
    )
  }
  upper <- x[-len] # nodes above eas
  lower <- x[-1L] # nodes below initial nodes
  child <- parent <- vector("list", length(upper))
  # Produce a list for each level with all the parent and child nodes.
  for (i in seq_along(upper)) {
    child[[i]] <- lapply(
      split(as.integer(lower[[len - i]]), upper[[len - i]]),
      unique
    )
    parent[[i]] <- lapply(
      split(as.integer(upper[[len - i]]), lower[[len - i]]),
      unique
    )
  }
  if (any(lengths(unlist(parent, recursive = FALSE)) > 1L)) {
    stop(
      "some nodes in the price index aggregation structure have ",
      "multiple parent nodes; the aggregation structure does not ",
      "represent a nested hierarchy"
    )
  }
  parent <- lapply(parent, unlist)
  levels <- lapply(x, levels)
  if (is.null(names(levels))) {
    names(levels) <- c(paste0("level", seq_along(child), recycle0 = TRUE), "ea")
  }
  piar_aggregation_structure(child, parent, levels, weights)
}
