#' Class for a schedule of pipelines
#' @import R6 tictoc utils logger lifecycle
#' @examples
#' if (interactive()) {
#'   pipeline_dir <- tempdir()
#'   create_pipeline("my_new_pipeline", pipeline_dir, open = FALSE)
#'   schedule <- build_schedule(pipeline_dir = pipeline_dir)
#' }
MaestroSchedule <- R6::R6Class(

  "MaestroSchedule",

  public = list(

    #' @field PipelineList object of type MaestroPipelineList
    PipelineList = NULL,

    #' @description
    #' Create a MaestroSchedule object
    #' @param Pipelines list of MaestroPipelines
    #' @return MaestroSchedule
    initialize = function(
      Pipelines = NULL
    ) {
      self$PipelineList <- MaestroPipelineList$new()
      pipeline_list <- purrr::map(Pipelines, ~.x$MaestroPipelines) |>
        purrr::list_flatten()
      priorities <- purrr::map(pipeline_list, ~.x$get_priority()) |>
        purrr::list_c()
      if (length(priorities) == 0) priorities <- list()
      pipeline_list <- pipeline_list[order(priorities)]
      self$PipelineList$MaestroPipelines <- pipeline_list
      self$PipelineList$n_pipelines <- length(pipeline_list)
    },

    #' @description
    #' Print the schedule object
    #' @return print
    print = function() {
      cli::cli_h3("Maestro Schedule with {length(self$PipelineList$MaestroPipelines)} pipeline{?s}: ")
      switch (private$sch_status,
        `Not Run` = cli::cli_li(cli::col_magenta(private$sch_status)),
        `Success` = cli::cli_li(cli::col_green(private$sch_status)),
        `Warning` = cli::cli_li(cli::col_yellow(private$sch_status)),
        `Error` = cli::cli_li(cli::col_red(private$sch_status)),
        cli::cli_li("Unknown")
      )
    },

    #' @description
    #' Run a MaestroSchedule
    #' @param ... arguments passed to MaestroPipelineList$run
    #' @param quiet whether or not to silence console messages
    #' @param run_all run all pipelines regardless of the schedule (default is `FALSE`) - useful for testing.
    #' @param n_show_next show the next n scheduled pipes
    #'
    #' @return invisible
    run = function(..., quiet = FALSE, run_all = FALSE, n_show_next = 5) {

      dots <- rlang::list2(..., quiet = quiet)
      check_datetime <- dots$check_datetime
      if (is.null(check_datetime)) {
        check_datetime <- lubridate::now()
      }

      is_multicore <- FALSE
      cores <- dots$cores
      if (!is.null(cores)) {
        if (cores < 1 || (cores %% 1) != 0) cli::cli_abort("`cores` must be a positive integer")
        if (cores > 1) {
          tryCatch({
            rlang::check_installed("furrr")
            is_multicore <- TRUE
          }, error = \(e) {
            cli::cli_warn("{.pkg furrr} is required for running on multiple cores.")
            cores <- 1
          })
        }
      }

      if (self$PipelineList$n_pipelines == 0) {
        cli::cli_inform("No pipelines in the schedule. Nothing to do.")
        return(invisible())
      }

      tryCatch({
        
        pipes_to_run <- self$PipelineList

        if (private$sch_status != "Not Run") {
          self$PipelineList$reset_pipelines()
        }

        if (!run_all) {
          pipes_to_run <- do.call(pipes_to_run$get_timely_pipelines, dots)
        }

        if (!quiet) {
          cli::cli_h3(
            "[{format(check_datetime, '%Y-%m-%d %H:%M:%S')}]
        Running pipelines {cli::col_green(cli::symbol$play)}")
        }

        tictoc::tic(quiet = quiet)
        pipes_that_ran <- do.call(pipes_to_run$run, dots)
        if (is_multicore) {
          self$PipelineList$update_pipelines(pipes_that_ran)
          # Collect pipelines whose inputs span independent branches were never
          # triggered inside any worker. Run them now with fully-synced state,
          # using run_pipe internally so their own downstream outputs recurse.
          do.call(self$PipelineList$run_pending_collects, dots)
        }
        elapsed <- tictoc::toc(quiet = TRUE)
        elapsed_ms <- elapsed$toc - elapsed$tic

        if (!quiet) {
          cli::cli_h3(
            "[{format(check_datetime + lubridate::milliseconds(elapsed_ms), '%Y-%m-%d %H:%M:%S')}]
            Pipeline execution completed {cli::col_silver(cli::symbol$stop)} | {elapsed$callback_msg}"
          )
        }

        run_errors <- self$PipelineList$get_errors()
        run_warnings <- self$PipelineList$get_warnings()
        run_messages <- self$PipelineList$get_messages()

        # For access via last_run_*
        maestro_pkgenv$last_run_errors <- run_errors
        maestro_pkgenv$last_run_warnings <- run_warnings
        maestro_pkgenv$last_run_messages <- run_messages

        status_table <- self$PipelineList$get_status()

        # Get the number of statuses
        invoked <- sum(status_table$invoked)
        error_count <- length(run_errors)
        success_count <- invoked - error_count
        warning_count <- length(run_warnings)

        if (!quiet) {
          cli::cli_text(
            "
        {cli::col_green(cli::symbol$tick)} {success_count} success{?es} |
        {cli::col_magenta('!')} {warning_count} warning{?s} |
        {cli::col_red(cli::symbol$cross)} {error_count} error{?s} |
        {cli::col_cyan(cli::symbol$square_small_filled)} {invoked} total
        "
          )

          if (error_count > 0) {
            cli::cli_alert_danger(
              "Use {.fn last_run_errors} to show pipeline errors."
            )
          }

          if (warning_count > 0) {
            cli::cli_alert_warning(
              "Use {.fn last_run_warnings} to show pipeline warnings."
            )
          }

          cli::cli_rule()

          # Output for showing next pipelines schedule
          if (!run_all && n_show_next > 0 && nrow(status_table) > 0) {

            next_runs_cli <- status_table |>
              dplyr::filter(!is.na(next_run)) |>
              dplyr::arrange(next_run) |>
              utils::head(n = n_show_next)

            cli::cli_h3("Next scheduled pipelines {cli::col_cyan(cli::symbol$pointer)}")
            next_run_strs <- glue::glue("{next_runs_cli$pipe_name} | {next_runs_cli$next_run}")
            cli::cli_text("Pipe name | Next scheduled run")
            cli::cli_ul(next_run_strs)
          }
        }
      }, error = function(e) {
        private$sch_status <- "Error"
        cli::cli_abort(
          "Failed to execute orchestrator with error: {e}"
        )
      }, warning = function(w) {
        private$sch_status <- "Warning"
        cli::cli_warn(
          "Orchestrator warned with {w}"
        )
      })
      private$sch_status <- "Success"

      return(invisible())
    },

    #' @description
    #' Get the schedule as a data.frame
    #' @return data.frame
    get_schedule = function() {
      self$PipelineList$get_schedule()
    },

    #' @description
    #' Get status of the pipelines as a data.frame
    #' @return data.frame
    get_status = function() {
      self$PipelineList$get_status() |> 
        dplyr::select(-internal_run_id)
    },

    #' @description
    #' Get artifacts (return values) from the pipelines
    #' @return list
    get_artifacts = function() {
      self$PipelineList$get_artifacts()
    },

    #' @description
    #' Get the network structure of the pipelines as an edge list (will be empty if there are no DAG pipelines)
    #' @return data.frame
    get_network = function() {
      self$PipelineList$get_network()
    },

    #' @description
    #' Get all pipeline flags as a long data.frame
    #' @return data.frame
    get_flags = function() {
      flag_list <- self$PipelineList$get_flags()
      purrr::imap(flag_list, ~{
        dplyr::tibble(
          pipe_name = .y,
          flag = .x
        )
      }) |>
        purrr::list_rbind()
    },

    #' @description
    #' Get all pipeline labels as a data.frame
    #' @return data.frame
    get_labels = function() {
      self$PipelineList$get_labels()
    },

    #' @description
    #' Get full sequence of scheduled executions for all pipelines
    #' @param n optional sequence limit
    #' @param min_datetime optional minimum datetime
    #' @param max_datetime optional maximum datetime
    #' @param include_only_primary only primary pipelines are included 
    #'   (this are pipelines that are scheduled and not downstream nodes in a DAG)
    #' @param include_skipped whether to include pipelines tagged with `@maestroSkip`
    #'   (default `TRUE` for backwards compatibility)
    #' @return data.frame
    get_run_sequence = function(n = NULL, min_datetime = NULL, max_datetime = NULL, include_only_primary = FALSE, include_skipped = TRUE) {

      pipeline_list <- self$PipelineList$MaestroPipelines

      if (!include_skipped) {
        pipeline_list <- purrr::discard(pipeline_list, ~isTRUE(.x$get_schedule()$skip))
      }

      run_sequence <- purrr::map(pipeline_list, ~.x$get_run_sequence(n = n, min_datetime = min_datetime, max_datetime = max_datetime)) |>
        stats::setNames(purrr::map_chr(pipeline_list, ~.x$get_schedule()$pipe_name)) |>
        purrr::discard(is.null) |>
        purrr::imap(
          ~dplyr::tibble(
            pipe_name = .y,
            scheduled_time = .x
          )
        ) |>
        purrr::list_rbind() |>
        dplyr::filter(!is.na(scheduled_time))

      network <- self$get_network()
      network$root <- find_roots(network$from, network$to)

      # Primary = not a downstream DAG node
      downstream_nodes <- network$to

      if (!include_only_primary) {
        run_sequence_dags <- NULL
        if (nrow(network) > 0) {
          run_sequence_dags <- purrr::map2(network$to, network$root, ~{
            run_sequence |>
              dplyr::filter(pipe_name == .y) |>
              dplyr::mutate(pipe_name = .x)
          }) |>
            purrr::list_rbind()
        }

        run_sequence <- run_sequence |>
          dplyr::bind_rows(run_sequence_dags) |>
          dplyr::arrange(scheduled_time)
      }

      run_sequence |>
        dplyr::mutate(is_primary = !pipe_name %in% downstream_nodes)
    }
  ),

  private = list(
    sch_status = "Not Run"
  )
)
