#' Add Content from PDF Documents
#'
#' @description Extract the text or cover image from publication PDF documents and
#'   save the result to disk, for incorporation into the \pkg{inlpubs} package corpus.
#'   Publications are selected by identifier or by year, and each must already be
#'   present in the `pubs` dataset before this function is run.
#'
#' @details For each selected publication, the function locates its primary PDF (the
#'   first file listed in the publication's `files` entry) within the source directory,
#'   then delegates extraction to [`get_pdf_text`] (for `type = "text"`) or
#'   [`get_pdf_image`] (for `type = "image"`). Extra arguments passed through `...`
#'   are forwarded to whichever of those functions is used.
#'
#'   Source documents are expected to follow the archive layout
#'   `srcdir/<year>/<pub_id>/<file>`, for example
#'   `archive/2005/KnobelOthers2005/ofr20051223.pdf`. Output files are written to
#'   `destdir` and named `pub-<pub_id>`, with the appropriate extension (`.txt` or
#'   `.jpg`) supplied by the extraction function.
#'
#'   Publications whose `files` entry is empty, and those listed in `ignore`, are
#'   skipped with a message. By default, existing output files are left in place;
#'   set `overwrite = TRUE` to regenerate them.
#'
#'   Extraction failures (including renderer crashes or timeouts on problematic
#'   PDFs) are caught per publication: the offending document is reported and
#'   skipped, and processing continues with the next one. The identifiers of any
#'   failed documents are returned in the `"failed"` attribute of the result.
#'
#'   Processing the full archive can take a considerable amount of time. For routine
#'   use, run this function interactively and one publication year at a time so that
#'   progress can be monitored.
#'
#' @param pub_id 'character' vector.
#'   Unique identifier(s) of the publication(s) to process.
#'   Publications may instead, or additionally, be selected with `year`.
#'
#' @param year 'integer' vector.
#'   Year(s) of publication. All publications from the given year(s) are selected,
#'   in addition to any specified by `pub_id`.
#'
#' @param type 'character' string.
#'   Type of content to extract from each PDF, either `"text"` (the default) or
#'   `"image"` (a cover image).
#'
#' @param ...
#'   Additional arguments passed to the extraction function:
#'   [`get_pdf_text`] when `type = "text"`, or [`get_pdf_image`] when `type = "image"`.
#'
#' @param srcdir 'character' string.
#'   Source directory containing the publication PDFs, organized into
#'   year subdirectories (see Details). Defaults to `"archive"` in the working directory.
#'
#' @param destdir 'character' string.
#'   Destination directory for the extracted output files. Created if it does not
#'   already exist. Defaults to the session's temporary directory.
#'
#' @param ignore 'character' vector.
#'   Publication identifier(s) to skip. Useful for documents that are known to fail
#'   automatic extraction.
#'
#' @param pubs 'pub' table.
#'   Publications of the INLPO; see the [`pubs`] dataset for the expected data format.
#'
#' @param overwrite 'logical' flag.
#'   Whether to overwrite an existing output file. Defaults to `FALSE`, in which case
#'   publications with an existing output are skipped.
#'
#' @return Invisibly returns a 'character' vector of paths to the saved output files,
#'   carrying a `"failed"` attribute with the identifiers of any publications whose
#'   extraction failed. Returns `NULL` (invisibly) when no publications remain to
#'   process.
#'
#' @author J.C. Fisher, U.S. Geological Survey, Idaho Water Science Center
#'
#' @export
#'
#' @seealso [`get_pdf_text`] and [`get_pdf_image`] for the underlying extraction
#'   functions, and the [`pubs`] dataset for publication metadata.
#'
#' @examples
#' \dontrun{
#'   # Extract text for a single publication into the corpus folder
#'   add_content("KnobelOthers2005", destdir = "data-raw/corpus")
#'
#'   # Extract text for every publication from a given year
#'   add_content(year = 2005, destdir = "data-raw/corpus")
#'
#'   # Extract cover images into the vignettes folder
#'   add_content(year = 2005, type = "image", destdir = "vignettes")
#'
#'   # Process several years, skipping a document that fails extraction
#'   add_content(year = 2003:2005, ignore = "Mazurek2004", destdir = "data-raw/corpus")
#' }

add_content <- function(
  pub_id,
  year,
  type = c("text", "image"),
  ...,
  srcdir = "archive",
  destdir = tempdir(),
  ignore = NULL,
  pubs = inlpubs::pubs,
  overwrite = FALSE
) {
  # check arguments
  if (missing(pub_id)) {
    pub_id <- NULL
  }
  if (!missing(year)) {
    checkmate::assert_integerish(year, lower = min(pubs$year), upper = max(pubs$year), any.missing = FALSE)
    ids <- pubs$pub_id[pubs$year %in% unique(year)]
    pub_id <- c(pub_id, ids) |> unique()
  }
  checkmate::assert_subset(pub_id, choices = pubs$pub_id, empty.ok = TRUE)
  type <- match.arg(type)
  checkmate::assert_directory_exists(srcdir, access = "r")
  checkmate::assert_string(destdir, min.chars = 1)
  checkmate::assert_subset(ignore, choices = pubs$pub_id, empty.ok = TRUE)
  checkmate::assert_class(pubs, classes = c("pub", "data.frame"), null.ok = TRUE)
  checkmate::assert_flag(overwrite)

  # set extraction method
  extract <- switch(type, "text" = get_pdf_text, "image" = get_pdf_image)

  # remove ignored publication identifiers
  pub_id <- pub_id[!pub_id %in% ignore]

  # account for no publications
  if (length(pub_id) == 0) {
    return(invisible(NULL))
  }

  # look up each publication's primary document (first file listed)
  idxs <- match(pub_id, pubs$pub_id)
  years <- pubs$year[idxs]
  docs <- vapply(
    pubs$files[idxs],
    FUN = function(x) if (length(x) == 0) NA_character_ else x[1],
    FUN.VALUE = character(1)
  )

  # drop publications with a missing document, keeping all vectors aligned
  if (any(missing_doc <- is.na(docs))) {
    n_missing <- sum(missing_doc)
    if (requireNamespace("cli", quietly = TRUE)) {
      cli::cli_alert_info(
        "Skipping {n_missing} publication{?s} with no archived document"
      )
    } else {
      message("Skipping ", n_missing, " publication(s) with no archived document")
    }
    pub_id <- pub_id[!missing_doc]
    years <- years[!missing_doc]
    docs <- docs[!missing_doc]
  }

  # account for no remaining publications
  if (length(pub_id) == 0) {
    return(invisible(NULL))
  }

  # build input (source PDF) and output paths
  inputs <- file.path(srcdir, years, pub_id, docs)
  outputs <- file.path(destdir, sprintf("pub-%s", pub_id))

  # create destination directory
  dir.create(destdir, showWarnings = FALSE, recursive = TRUE)

  # extract content for each publication. Extraction is wrapped so that a failure
  # on one document (e.g. a renderer crash or timeout caught by the extraction
  # function) is reported and skipped rather than aborting the whole run. Failed
  # publications yield NA here and are recorded for the returned "failed" attribute.
  results <- vapply(
    seq_along(inputs),
    FUN = function(i) {
      id <- sQuote(pub_id[i])
      message("Extract publication ", id)

      # skip when output already exists and overwrite is not requested
      if (!overwrite && checkmate::test_file_exists(outputs[i], access = "rw")) {
        message("Skipping publication ", id, " (output exists)")
        return(outputs[i])
      }

      # skip when the source PDF is not present
      if (!checkmate::test_file_exists(inputs[i], access = "r")) {
        message("Missing source PDF for publication ", id)
        return(NA_character_)
      }

      # extract, catching errors (including the renderer crash/timeout errors
      # that the extraction functions raise) so one bad document cannot abort
      # the run
      tryCatch(
        extract(input = inputs[i], output = outputs[i], ...),
        error = function(e) {
          message("Failed to extract publication ", id, ": ", conditionMessage(e))
          NA_character_
        }
      )
    },
    FUN.VALUE = character(1)
  )

  # separate successes from failures
  failed <- pub_id[is.na(results)]
  paths <- results[!is.na(results)]

  if (length(failed) > 0) {
    message(
      "Failed to extract ",
      length(failed),
      " publication(s): ",
      paste(sQuote(failed), collapse = ", ")
    )
  }

  attr(paths, "failed") <- failed
  invisible(paths)
}
