stepwise <- function(target_model, direction="forward", criterion="pvalue", adjust_method = "fdr", alpha.level=0.05, trace=TRUE) {
  # Validation
  if (!(direction %in% c("forward", "backward", "both_null", "both_full", "bestsubset", "onepass"))) {
    stop("Error: direction must be 'forward', 'backward', 'both_null', 'both_full', 'bestsubset', or 'onepass'.")
  }
  if (!(criterion %in% c("pvalue", "AIC", "BIC"))) {
    stop("Error: criterion must be 'pvalue', 'AIC', or 'BIC'")
  }
  if (!(adjust_method %in% c("holm", "hochberg", "hommel", "bonferroni", "BH", "BY", "fdr", "none"))) {
    stop("Error: adjust_method must be 'holm', 'hochberg', 'hommel', 'bonferroni', 'BH', 'BY', 'fdr', or 'none'.")
  } 
  if (is.null(target_model$covariates) || is.null(target_model$X)) {
    stop("Error: Target model must have covariates and a data matrix.")
  }
  if (ncol(target_model$X) != length(target_model$covariates)) {
    stop("Error: Target model must incorporate all covariates.")
  }
  if (is(target_model, "ARCHmX") || is(target_model, "ARCHmXAdditive")) {
    if (target_model$hypothesisTest == FALSE) {
      stop("Error: Target model requires hypothesis test.")
    }
  }
  if ((!is(target_model, "GARCHX")) && (!is(target_model, "ARCHmX"))  && (!is(target_model, "ARCHmXAdditive"))) {
    stop("Error: This function can only be called on GARCHX, ARCHmX, and ARCHmXAdditive objects.")
  }

  # Steps taken structure
  steps_taken <- structure(
    list(
      covariates = c(),
      aic = c(),
      bic = c(),
      maxpval = c(),
      current_model = list()
    )
  )
  
  generate_candidate_model <- function(target_model, covariates) {
    if (is(target_model, "GARCHX")) {
      return(new_GARCHX(eps = target_model$eps,
                        X = target_model$X,
                        order = target_model$order,
                        delta = target_model$delta,
                        covariates = covariates,
                        optim.method = target_model$optim.method
                        ))
    } else if (is(target_model, "ARCHmX")) {
      return (new_ARCHmX(X = target_model$X,
                         p = target_model$p,
                         eps = target_model$eps,
                         delta = target_model$delta, 
                         covariates = covariates, 
                         bandwidth = target_model$bandwidth,
                         k.choice = target_model$k.choice
                         ))
    } else if (is(target_model, "ARCHmXAdditive")) {
      return(new_ARCHmXAdditive(X = target_model$X,
                                p = target_model$p,
                                eps = target_model$eps,
                                delta = target_model$delta,
                                covariates = covariates,
                                method = target_model$method,
                                train_end = target_model$train_end,
                                n_burn = target_model$n_burn,
                                max_outer_iter = target_model$max_outer_iter,
                                tol = target_model$tol,
                                hypothesisTest = TRUE,
                                k.choice = target_model$k.choice
                                ))
    }
  }
  
  # Helper functions for main logic below
  
  # rewritten 11/20 to take advantage of new model covariate field
  store_model <- function(model, steps_taken) {
    if (is.null(model$covariate_p.values)) { # empty model
      pvalue <- c("NA")
    } else {
      pvalue <- max(p.adjust(model$covariate_p.values, method = adjust_method))
    }
    new_steps_taken <- structure(
      list(
        covariates = c(steps_taken$covariates, paste(model$covariates, collapse=", ")),
        aic = c(steps_taken$aic, AIC(model)),
        bic = c(steps_taken$bic, BIC(model)),
        maxpval = c(steps_taken$maxpval, pvalue),
        current_model = model
      )
    )
    return(new_steps_taken)
  }
  
  # 11/23 - maybe it might make sense to change how
  # ARCH_internal.R and ARCHmX.R work instead of 
  # using this workaround to get the model covariate count
  covariate_count <- function(model) {
    if (is.null(model$covariates)) { # ARCH(p) "empty" model
      return(0)
    } else {
      return(length(model$covariates))
    }
  }
  
  # reviewed 11/20, i think this is still good
  combinations <- function(current_covariates, size) {
    # returns a matrix of row-wise covariate combinations
    # We only need to handle the forward and backward directions 
    # since the both logic will just call this function twice
    if (is.null(current_covariates)) {
      current_covariates <- c()
    }
    if (length(current_covariates) < size) { # This is the forward direction
      combs <- t(combn(ncol(target_model$X), size))
      for (index in current_covariates) {
        row_contains_index <- apply(combs, MARGIN = 1, FUN = function(row) {index %in% row})
        combs <- matrix(combs[row_contains_index, ], ncol=size)
      }
    } else if (length(current_covariates) > size) { # this is the backward direction
      combs <- t(combn(current_covariates, size)) # size should always be length(sig...) - 1
    }
    return(combs)
  }
  
  # reviewed 11/20, looks good
  # 1/12/26 added functionality to handle common_df scenario. See comments on line 204.
  metric <- function(model, criterion, common_df=FALSE) {
    if(is.null(model$covariates)) 
    {
      offset = 1
    } else {
      offset = 0
    }
    if (criterion == "AIC") 
    {
      if(common_df) {
        df <- model$p + offset
        log_QL <- model$log_QL
        return(2 * df - 2 * log_QL)
      }
      else {
        return(AIC(model))
      }
    }
    else if (criterion == "BIC") {
      if(common_df) {
        p <- model$p
        df <- p + offset
        T_ <- length(model$eps)
        log_QL <- model$log_QL
        return(df * log(T_-p) - 2 * log_QL)
      }
      else {
        return(BIC(model))
      }
    }
    else { # criterion == "pvalue"
      if (!is.null(model$covariate_p.values)) {
        return(max(p.adjust(model$covariate_p.values, method = adjust_method)))
      } else { # empty model
        return(NA) 
      }
    }
  }
  
  # written 11/21, updated 11/26
  step_difference <- function(current, proposed) {
    current_count <- covariate_count(current)
    proposed_count <- covariate_count(proposed)
    if (current_count == proposed_count) {
      return("<none>")
    } else if (current_count < proposed_count) { # forward (additive) step
      for (j in 1:proposed_count) {
        if (!(as.character(proposed$covariates[j]) %in% as.character(current$covariates))) {
          return(paste("+", as.character(proposed$covariates[j])))
        }
      }
    } else { # length(covs1) > length(covs2), backward (reductive) step
      for (i in 1:current_count) {
        if (!(as.character(current$covariates[i]) %in% as.character(proposed$covariates))) {
          return(paste("-", as.character(current$covariates[i])))
        }
      }   
    }
  }
  
  # 1/12/26 helper function to identify comparisons against null model
  contains_null_model <- function(candidates) {
    null_model_found <- FALSE
    for (i in 1:length(candidates)) {
      null_model_found <- null_model_found || is.null(candidates[[i]]$X)
    }
    return(null_model_found)
  }
  
  # written 11/21
  display_step <- function(candidates, ranked_candidate_indices, step_to_index) {
    row_names <- c()
    AIC <- c()
    BIC <- c()
    max_pvalue <- c()
    pass.correction <- c()
    
    # 1/12/26: If candidates include the null model, we need to get an apples-to-apples comparison.
    #          To do so, 'metric' function will treat all models as having df=1+P (what we've 
    #          termed 'common_df') when computing AIC or BIC. 
    common_df <- contains_null_model(candidates)
    
    for (index in ranked_candidate_indices) {
      row_names <- c(row_names, step_difference(candidates[[1]], candidates[[index]]))
      AIC <- c(AIC, metric(candidates[[index]], "AIC", common_df))
      BIC <- c(BIC, metric(candidates[[index]], "BIC", common_df))
      max_pvalue <- c(max_pvalue, metric(candidates[[index]], "pvalue"))
    }
    pass.correction <- (max_pvalue < alpha.level)
    selected <- rep("", length(candidates))
    selected[which(step_to_index == ranked_candidate_indices)] <- "*"
    
    cat("Call:\n")
    cat(class(target_model),"(X, p, eps, delta=", candidates[[1]]$delta, ", covariates=",
        paste(candidates[[1]]$covariates,sep=","),
        ")\n\n", sep="")
    
    cat("Covariates:\n")
    cat(paste(candidates[[1]]$covariates, sep=", "), "\n\n")
    
    step.df <- data.frame(
      #Resid.Dev, AIC, BIC, max_pvalue, pass.correction, selected,
      AIC, BIC, max_pvalue, pass.correction, selected,
      row.names = row_names
    )
    
    print(step.df)
    cat("\n\n")
  }
  
  # rewritten 11/22
  attempt_step <- function(candidates, step_direction="forward") { 
  # can't call the parameter 'direction' because I need to be able to step forward and backward within the 'both' logic
    
    # 1/12/26: If candidates include the null model, we need to get an apples-to-apples comparison.
    #          To do so, 'metric' function will treat all models as having df=1+P (what we've 
    #          termed 'common_df') when computing AIC or BIC. 
    common_df <- contains_null_model(candidates)
    
    max_adj_pval <- rep(0, length(candidates))
    metrics <- rep(0, length(candidates))
    for (i in 1:length(candidates)) {
      # populate the max_adj_pval vector, handling the empty model if it comes up
      # The adjustments below ensure the correct step happens.
      
      # Forward step from empty model to univariate model:
      #     P value metric: Empty model has metric 1 so the prospective models will always improve the metric.
      #                     The step will happen as long as some prospective univariate model has significant p-value. 
      #                     This is exactly what should happen based on the hypothesis test for the univariate model. 
      #   Any other metric: The empty model cannot be a prospective model since it doesn't improve on its own metric.
      #                     So it doesn't matter what its p value is. We can only step to some other model.
      
      # Backward step from univariate model to empty model:
      #     P value metric: Empty model has metric 0 so the step will always happen whenever the univariate model
      #                     has insignificant p-value. This is exactly what should happen based on the hypothesis
      #                     test for the univariate model.
      #   Any other metric: The step will happen only when there is metric improvement. 

      if (is.na(metric(candidates[[i]], "pvalue"))) {
        if (step_direction == "forward") {
          max_adj_pval[i] <- 1
        } else if (step_direction == "backward") {
          max_adj_pval[i] <- 0
        } else {
          # p value cannot be the metric for both selection, 
          # so we are only using it to decide whether we may take forward steps
          # We can only move forward FROM the empty model, so it doesn't matter
          #       what we do with the p-value, because the current doesn't improve the metric
          # We can only move backward TO the empty model, and again it doesn't matter
          #       what we do with the p-value, because backward steps for non-p value metrics
          #       don't care about p value.
          # So, we will just make the p-value 0 to have a way to identify the null model when we find it
          max_adj_pval[i] <- 0
        }
      } else {
        max_adj_pval[i] <- metric(candidates[[i]], "pvalue")
      }
      # populate the metrics vector
      if (criterion == "pvalue") {
        metrics[i] <- max_adj_pval[i]
      } else {
        metrics[i] <- metric(candidates[[i]], criterion, common_df)
      }
    }

    ranked_candidate_indices <- order(metrics) # we want to minimize all metrics, so default ascending order is correct
    metric_improved <- (metrics[ranked_candidate_indices] < metrics[1])
    
    if (step_direction == "forward") {
      
      # We step to the model with most significant metric improvement, which also has all significant adjusted p-values.
      # If none fit the criteria, we stay with the current model.
    
      all_pvals_signif <- (max_adj_pval[ranked_candidate_indices] < alpha.level)
      
      # 12/21: slight modification to force step when metric is p-value and there are candidate models
      # with all significant p-values (as originally written, if min(max(pvalue)) belongs to the current model,
      # step doesn't happen)
      
      if (criterion == "pvalue") {
        better_model_indices <- ranked_candidate_indices[all_pvals_signif]
        better_model_indices <- better_model_indices[which (better_model_indices != 1)]
      } else {
        better_model_indices <- ranked_candidate_indices[metric_improved & all_pvals_signif]
      }
      
      #  better_model_indices <- ranked_candidate_indices[metric_improved & all_pvals_signif]
      
      if (length(better_model_indices) == 0) { # no improved model with all significant p-values
        step_to_index <- 1
      } else {
        step_to_index <- better_model_indices[1]
      }
      
    } else if (step_direction == "backward") {
      
      # We step to the model with the most significant metric improvement,
      # unless the current model has all significant adjusted p-values
      
      better_model_indices <- ranked_candidate_indices[metric_improved]
      
      #if ( (length(better_model_indices) == 0) | (max_adj_pval[1] < alpha.level) ) {
      #  step_to_index <- 1
      #} else {
      #  step_to_index <- better_model_indices[1]
      #}
      
      # if the current model has all significant p-values, stop.
      if (max_adj_pval[1] < alpha.level) {
        step_to_index <- 1
        # if no reduced model outperforms the current model, pick whichever one does the best
      } else if (length(better_model_indices) == 0) {
        step_to_index <- ranked_candidate_indices[2]
        # otherwise, we step to the best overall model
      } else {
        step_to_index <- better_model_indices[1]
      }
      
    } else if (step_direction == "both") {
      # we only care about the p-value check when taking forward steps
      p_value_good <- rep(TRUE, length(ranked_candidate_indices))
      current_cov_count <- covariate_count(candidates[[1]])
      for (i in 1:length(ranked_candidate_indices)) {
        ranked_candidate_cov_count <- covariate_count(candidates[[ranked_candidate_indices[i]]])
        if (ranked_candidate_cov_count > current_cov_count) {
          p_value_good[i] <- (metric(candidates[[ranked_candidate_indices[i]]], "pvalue") < alpha.level)
        } else {
          p_value_good[i] <- TRUE
        }
      }
      
      better_model_indices <- ranked_candidate_indices[metric_improved & p_value_good]
      
      # we don't have to worry about the current model having all significant p-values because
      # in the main body of the both selection logic, we never built backward candidates in that case.
      if ( length(better_model_indices) == 0 ) {
        step_to_index <- 1
      } else {
        step_to_index <- better_model_indices[1]
      }
    }
    
    if (trace) {
      display_step(candidates, ranked_candidate_indices, step_to_index) 
    }
    return(candidates[[step_to_index]])
  }
  
  # Main program logic starts here!
  
  # rewrote this 11/21
  if (direction == "forward") {
    
    # build and store the empty model...
    #cat("Building empty model\n")
    empty_model <- GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta) 
    #empty_model <- ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta)
    steps_taken <- store_model(empty_model, steps_taken)
    
    # ...then start stepping
    for (size in 1:ncol(target_model$X)) { 
      # candidates vector contains all models to choose from at this step, index 1 is the current model
      candidates <- list()
      candidates[[1]] <- steps_taken$current_model
      current_covariates <- candidates[[1]]$covariates
      combs <- combinations(current_covariates, size)
      for (i in 1:nrow(combs)) {
        combination <- combs[i,]
        #cat("Building", combination, "\n")
        #candidates[[i+1]] <- ARCHmX(X, p, eps, covariates=combination, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice)
        candidates[[i+1]] <- generate_candidate_model(target_model, covariates = combination) #### ?????????
      }
      
      # attempt a step...
      best_model <- attempt_step(candidates, step_direction="forward")    # ????
      if (identical(steps_taken$current_model, best_model)) {
        break # ...exit the loop if you don't take one
      } else {
        steps_taken <- store_model(best_model, steps_taken)
      }
    }

  } else if (direction == "backward") { 
    
    # build and store the full model...
    full_model <- target_model
    steps_taken <- store_model(full_model, steps_taken)
    
    # ...then start stepping
    for (size in ((ncol(target_model$X)-1):0)) {
      candidates <- list()
      candidates[[1]] <- steps_taken$current_model
      current_covariates <- candidates[[1]]$covariates
      if (size > 0) { # Candidates are covariate models
        combs <- combinations(current_covariates, size)
        for (i in 1:nrow(combs)) {
          combination <- combs[i,]
          #candidates[[i+1]] <- ARCHmX(X, p, eps, covariates=combination, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice)
          candidates[[i+1]] <- generate_candidate_model(target_model, covariates = combination)
        }
      } else { # Candidate is the empty model
        #candidates[[2]] <- ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta)
        candidates[[2]] <- GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta) 
      }

      # attempt a step...
      best_model <- attempt_step(candidates, step_direction="backward")
      if (identical(steps_taken$current_model, best_model)) {
        break # ...exit the loop if you don't take one
      } else {
        steps_taken <- store_model(best_model, steps_taken)
      }
    }
    
    # best subset logic written 11/23
  } else if (direction == "bestsubset") {
    # Retain the model with best metric performance and all significant p-values. 
    # If no model has all significant p-values, return the empty model.
    
    best_metric <- Inf # we want to minimize all metrics)
    best_model <- list()
    all_covariates <- 1:ncol(target_model$X)
    for (size in 1:ncol(target_model$X)) {
      combs <- combinations(all_covariates, size)
      for (i in 1:nrow(combs)) {
        combination <- combs[i,]
        #candidate<- ARCHmX(X, p, eps, covariates=combination, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice)
        candidate <- generate_candidate_model(target_model, covariates = combination)
        candidate_metric <- metric(candidate, criterion)
        candidate_max_adj_pval <- metric(candidate, "pvalue") 
        improves_metric <- (candidate_metric < best_metric)
        adj_pval_signif <- (candidate_max_adj_pval < alpha.level)
        if (improves_metric & adj_pval_signif) {
          best_metric <- candidate_metric
          best_model <- candidate
        }
      }  
    }
    if (best_metric == Inf) { # if no candidate found...
      #steps_taken <- store_model(ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta), steps_taken) # ...return the empty model
      steps_taken <- store_model(GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta), steps_taken) # ...return the empty model
    } else { # otherwise return the best candidate
      steps_taken <- store_model(best_model, steps_taken)
    }

    # both directions case
  } else if (direction %in% c("both_null", "both_full")) {
    # Validation
    if (criterion == "pvalue") {
      #stop("Error: criterion 'pvalue' cannot be used for 'both'.")
      stop("Error: criterion 'pvalue' cannot be used for 'both_null' or 'both_full'.")
    }
    
    # Set flag
    converged <- FALSE
    
    # Build initial model 
    if (direction == "both_null") {
      #initial_model <- ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta)
      initial_model <- GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta)
    } else { # direction == "both_full"
      #initial_model <- ARCHmX(X_initial, p, eps, delta=delta, hypothesisTest=TRUE)
      initial_model <- target_model
    }
    steps_taken <- store_model(initial_model, steps_taken)
    
    while(!converged) {
      # get current model
      candidates <- list()
      candidates[[1]] <- steps_taken$current_model
      current_covariate_count <- covariate_count(candidates[[1]])
      
      # get forward candidates if current model not full model
      if (current_covariate_count < ncol(target_model$X)) {
        current_covariates <- candidates[[1]]$covariates
        forward_combs <- combinations(current_covariates, current_covariate_count + 1)
        for (i in 1:nrow(forward_combs)) {
          combination <- forward_combs[i,]
          #candidates[[1 + i]] <- ARCHmX(X, p, eps, covariates=combination, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice)
          candidates[[1 + i]] <- generate_candidate_model(target_model, covariates = combination)
        }
      }
      
      # we only want backward candidates when 
      #   1. current model is not the empty model
      #   2. current model doesn't have all significant p values
      current_model_empty <- (current_covariate_count == 0)
      if (current_model_empty) {
        all_pvals_signif <- TRUE
      } else {
        all_pvals_signif <- metric(candidates[[1]], "pvalue") <= alpha.level
      }
      if (!(current_model_empty | all_pvals_signif)) {
        n_candidates <- length(candidates)
        if (current_covariate_count >= 2) {
          backward_combs <- combinations(candidates[[1]]$covariates, current_covariate_count - 1)
          for (i in 1:nrow(backward_combs)) {
            combination <- backward_combs[i,]
            #candidates[[n_candidates + i]] <- ARCHmX(X, p, eps, covariates=combination, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice)
            candidates[[n_candidates + i]] <- generate_candidate_model(target_model, covariates = combination)
          }
        } else { #length(current_covariates)==1 so the backwards candidate is the empty model
          #candidates[[n_candidates + 1]] <- ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta)
          candidates[[n_candidates + 1]] <- GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta)
          
        }
      }
      
      best_model <- attempt_step(candidates, step_direction="both")
      
      if (identical(steps_taken$current_model, best_model)) {
        converged <- TRUE
      } else {
        steps_taken <- store_model(best_model, steps_taken)
      }
    }
    # one-pass case, written 12/6
  } else {
    if (criterion != "pvalue") {
      stop("Error: criterion must be 'pvalue' to use 'onepass'.")
    }
    full_model <- target_model
    significant_covariates <- full_model$covariates[full_model$covariate_p.values < alpha.level]
    if (length(significant_covariates) > 0) {
      #return(ARCHmX(X, p, eps, covariates=significant_covariates, delta=delta, hypothesisTest=TRUE, bandwidth = bandwidth, k.choice = k.choice))
      return(generate_candidate_model(target_model, covariates=significant_covariates))
    }
    else {
      #return(ARCH_internal(eps=target_model$eps, p=target_model$p, delta=target_model$delta))
      return(GARCHX(eps = target_model$eps, X = NULL, order = c(target_model$p,0), delta = target_model$delta))
    }
  }
  
  cat("Selection process complete. Steps taken:\n\n")
  steps.info <- data.frame(Covariates = steps_taken$covariates, 
                           AIC = steps_taken$aic, 
                           BIC = steps_taken$bic, 
                           MaxPValue = steps_taken$maxpval)
  n_steps <- nrow(steps.info) - 1
  rownames(steps.info) <- 0:n_steps
  selected_model <- steps_taken$current_model
  selected_model$steps.info <- steps.info
  cat("Selection process complete. Total steps taken:", n_steps, "\n\nLast model returned as selected model.")
  return(selected_model)
}