#' Parse and validate tags then create and populate MaestroPipelineList
#'
#' @param script_path path to script
#'
#' @keywords internal
#'
#' @return MaestroPipelineList R6 class
build_schedule_entry <- function(script_path) {

  # Current list of maestro tags and their equivalent table names
  maestro_tag_names <- list(
    frequency = "maestroFrequency",
    start_time = "maestroStartTime",
    tz = "maestroTz",
    skip = "maestroSkip",
    log_level = "maestroLogLevel",
    hours = "maestroHours",
    days = "maestroDays",
    months = "maestroMonths",
    inputs = "maestroInputs",
    outputs = "maestroOutputs",
    maestro = "maestro",
    priority = "maestroPriority",
    flags = "maestroFlags",
    run_if = "maestroRunIf",
    map = "maestroMap"
  )

  # List of maestro tags that can be used more than once
  maestro_tag_names_mult <- list(
    labels = "maestroLabel"
  )

  # Initial Validation ------------------------------------------------------

  # Get all the roxygen tags - this actually executes the script, which is fine
  # if it's functions but can be problematic if it includes other code that
  # evaluates immediately
  tag_list <- tryCatch({
    parse_env <- new.env()
    parse <- function() roxygen2::parse_file(script_path)
    environment(parse) <- parse_env
    parse()
  }, error = \(e) {
    cli::cli_abort(
      c("Could not build schedule entry with error: {e$message}",
        "i" = "Pipeline scripts should not typically include code that's run
        outside of a function. Be sure to wrap code in a function block."),
      call = NULL
    )
  }, warning = \(w) {
    cli::cli_abort(w$message, call = NULL)
  }) |>
    suppressMessages()

  # Get specifically the tags used by maestro
  withCallingHandlers({
    maestro_tag_vals <- purrr::map(tag_list, ~{
      tag <- .x
      single_vals <- purrr::map(
        maestro_tag_names,
        ~{
          roxygen2::block_get_tag_value(tag, .x)
        }
      )
      multi_vals <- purrr::map(
        maestro_tag_names_mult,
        ~{
          roxygen2::block_get_tags(tag, .x) |> 
            purrr::map("val")
        }
      )
      append(single_vals, multi_vals)
    })
  },
  purrr_error_indexed = function(err) {
    rlang::cnd_signal(err$parent)
  })

  if (length(maestro_tag_vals) == 0) {
    cli::cli_abort(
      c("No {.pkg maestro} tags present in {basename(script_path)}.",
        "i" = "A valid pipeline must have at least one function with one or
        more {.pkg maestro} tags: e.g., `#' @maestroFrequency 1 day`."),
      call = NULL
    )
  }

  # Get pipe names from the function name and check
  withCallingHandlers({
    pipe_names <- purrr::imap(tag_list, ~{

      obj_class <- class(.x$object)

      # Check that it is a function
      if (!"function" %in% obj_class) {
        cli::cli_abort(
          c("{basename(script_path)} line {(.x$line)} has tags but no function. Be sure place
          tags above the function you want to schedule."),
          call = NULL
        )
      }

      # Return the name
      .x$object$topic
    })
  }, purrr_error_indexed = function(err) {
    rlang::cnd_signal(err$parent)
  })

  # Create MaestroPipelineList ----------------------------------------------

  maestro_pipeline_list <- MaestroPipelineList$new()

  withCallingHandlers({
    pipelines <- purrr::pmap(list(tag_list, pipe_names, maestro_tag_vals), ~{
      block  <- ..1
      .x <- ..2
      .y <- ..3

      # Validate inputs
      if (!all(is.null(.y$inputs))) {
        if (.x %in% .y$inputs) {
          cli::cli_abort(
            c("`@maestroInput` cannot contain self-references. Pipeline {.pkg .x} in {basename(script_path)}
              contains an input with the same name."),
            call = NULL
          )
        }
        params <- roxygen2::block_get_tag_value(block, ".formals")
        if (!".input" %in% params) {
          cli::cli_abort(
            c("If specifying `@maestroInputs` the pipeline must have a parameter named `.input`",
              "i" = "Example: {.code pipeline <- function(.input) ...}"),
            call = NULL
          )
        }
      }

      # Validate outputs
      if (!all(is.null(.y$outputs))) {
        if (.x %in% .y$outputs) {
          cli::cli_abort(
            c("`@maestroOutput` cannot contain self-references. Pipeline {.pkg .x} in {basename(script_path)}
              contains an output with the same name."),
            call = NULL
          )
        }
      }

      freq_nunits <- if (!is.null(.y$frequency)) parse_rounding_unit(.y$frequency) else NULL

      if (!is.null(freq_nunits)) {

        freq_unit <- freq_nunits$unit

        # Validate hours
        if (
          !all(is.null(.y$hours)) && !freq_unit %in% c("second", "minute", "hour")
        ) {
          cli::cli_abort(
            c("If specifying `@maestroHours` the pipeline must have a `@maestroFrequency` of at least 'hourly'.",
              "i" = "Issue is with pipeline named {.x}."),
            call = NULL
          )
        }

        # Validate days
        if (!all(is.null(.y$days)) &&
            !freq_unit %in% c("second", "minute", "hour", "day")) {
          cli::cli_abort(
            c("If specifying `@maestroDays` the pipeline must have a `@maestroFrequency` of at least 'daily'.",
              "i" = "Issue is with pipeline named {.x}."),
            call = NULL
          )
        }

        # Validate months
        if (!all(is.null(.y$months)) && !freq_unit %in%
            c("second", "minute", "hour", "day", "week", "month")) {
          cli::cli_abort(
            c("If specifying `@maestroMonths` the pipeline must have a `@maestroFrequency` of
            at least 'monthly'.",
              "i" = "Issue is with pipeline named {.x}."),
            call = NULL
          )
        }
      }

      tz <- .y$tz %n% "UTC"

      .weekday_abbrs <- c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
      is_time_only   <- !is.null(.y$start_time) &&
        grepl("^[0-9]{2}:[0-9]{2}:[0-9]{2}$", .y$start_time %n% "")
      is_weekday_fmt <- !is.null(.y$start_time) &&
        grepl(
          sprintf("^(%s)\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}$",
                  paste(.weekday_abbrs, collapse = "|")),
          .y$start_time %n% ""
        )
      is_monthday_fmt <- !is.null(.y$start_time) &&
        grepl("^[0-9]{1,2}(\\s+[0-9]{2}:[0-9]{2}:[0-9]{2})?$",
              .y$start_time %n% "")

      if (!is.null(freq_nunits)) {

        if (is_time_only) {
          if (freq_nunits$n > 1 && freq_nunits$unit == "day") {
            cli::cli_abort(
              c("Cannot use a `@maestroStartTime` with format %H:%M:%S in combination with
                a multi-unit daily `@maestroFrequency`.",
                "i" = "Instead use something like `daily` or `1 day`."),
              call = NULL
            )
          }
          if (freq_nunits$unit %in% c("week", "month", "year")) {
            cli::cli_abort(
              c("`@maestroStartTime` with format %H:%M:%S is only valid for `@maestroFrequency` of minute, hour, or day."),
              call = NULL
            )
          }
        }

        if (is_weekday_fmt && !freq_nunits$unit %in% c("week")) {
          cli::cli_abort(
            c("`@maestroStartTime` with weekday format (e.g., `Mon 04:00:00`) is only valid for weekly `@maestroFrequency`.",
              "i" = "Issue is with pipeline named {.x}."),
            call = NULL
          )
        }

        if (is_monthday_fmt && !freq_nunits$unit %in% c("month")) {
          cli::cli_abort(
            c("`@maestroStartTime` with month-day format (e.g., `15 04:00:00`) is only valid for monthly `@maestroFrequency`.",
              "i" = "Issue is with pipeline named {.x}."),
            call = NULL
          )
        }
      }

      # Pass the raw start_time string through; MaestroPipeline resolves it
      start_time_raw <- .y$start_time %n% NA_character_

      if (!is.null(.y$inputs)) {
        inputs <- .y$inputs$inputs
        is_collect <- .y$inputs$is_collect
      } else {
        inputs <- NULL
        is_collect <- FALSE
      }

      # Parse and validate @maestroMap
      map <- if (!is.null(.y$map)) {
        exprs <- .y$map  # character vector, one element per iterator
        # Validate that every element is parseable R
        bad <- purrr::keep(exprs, \(e) {
          inherits(tryCatch(str2lang(e), error = \(err) err), "error")
        })
        if (length(bad) > 0) {
          cli::cli_abort(
            c(
              "`@maestroMap` contains invalid R expression{?s}: {.code {bad}}",
              "i" = "Issue is with pipeline named {.x}."
            ),
            call = NULL
          )
        }
        exprs
      } else {
        NULL
      }

      # Create the new pipeline
      MaestroPipeline$new(
        script_path = script_path,
        pipe_name = .x,
        frequency = .y$frequency %n% "daily",
        start_time_raw = start_time_raw,
        tz = tz,
        skip = .y$skip %n% FALSE,
        log_level = .y$log_level %n% "INFO",
        hours = .y$hours %n% 0:23,
        days = .y$days %n% NULL,
        months = .y$months %n% 1:12,
        inputs = inputs,
        outputs = .y$outputs %n% NULL,
        priority = as.numeric(.y$priority %n% Inf),
        flags = .y$flags %n% character(),
        run_if = .y$run_if %n% NULL,
        is_collect = is_collect,
        map = map,
        labels = .y$labels
      )
    })
  }, purrr_error_indexed = function(err) {
    rlang::cnd_signal(err$parent)
  })

  maestro_pipeline_list$MaestroPipelines <- pipelines
  maestro_pipeline_list$n_pipelines <- length(pipelines)

  maestro_pipeline_list
}
