R/utils_model.R

Defines functions update_tidysem_labels_bayes update_tidysem_labels get_params_with_group_names get_comparison_table get_comparison_table_bayes get_est_differences_bayes get_est_differences combine_model_parameters combine_model_parameters_bayes combine_tidysem_objects_bayes combine_tidysem_objects combine_tidysem_groups_bayes combine_tidysem_groups get_openmx_parameters lavstring_to_fit lavaan_to_sempaths blavaan_to_sempaths blavaan_to_lavstring reconstruct_from_parTable fit_to_lavstring create_lavaan_with_fixed_params convert_openmx_to_lavaan convert_mx_to_lavaan detect_mx_model_type extract_mx_syntax extract_mplus_syntax try_auto_detect_model detect_mplus_model_type convert_mplus_to_lavaan openmx_to_lavaan

#' Convert OpenMx model to lavaan syntax
#'
#' Converts an OpenMx model object to lavaan model syntax string. Handles
#' both single-group and multi-group models, including submodels.
#'
#' @param mxModel An OpenMx model object to convert
#' @param group_prefix Character string prefix for group names in multi-group models
#'   (used internally for recursion)
#'
#' @return Character string containing lavaan model syntax
#' @keywords internal
#' @noRd
openmx_to_lavaan <- function(mxModel, group_prefix = NULL) {
  model_string <- c()

  # If model has submodels, recurse
  if (length(mxModel@submodels) > 0) {
    for (submodel in mxModel@submodels) {
      sub_prefix <- if (is.null(group_prefix)) submodel$name else paste0(group_prefix, ".", submodel$name)
      sub_syntax <- openmx_to_lavaan(submodel, group_prefix = sub_prefix)
      model_string <- c(model_string, paste0("# Submodel: ", sub_prefix), sub_syntax)
    }
    return(paste(model_string, collapse = "\n"))
  }

  # Extract matrices and variables
  A <- mxModel$A$values
  S <- mxModel$S$values
  manifests <- mxModel@manifestVars
  latents <- mxModel@latentVars

  # 1. Latent variable loadings
  for (latent in latents) {
    loading_vars <- c()
    for (manifest in manifests) {
      if (!is.na(A[manifest, latent]) && A[manifest, latent] != 0) {
        loading_vars <- c(loading_vars, manifest)
      }
    }
    if (length(loading_vars) > 0) {
      line <- paste0(latent, " =~ ", paste(loading_vars, collapse = " + "))
      model_string <- c(model_string, line)
    }
  }

  # 2. Residual variances for manifests
  for (manifest in manifests) {
    var_value <- S[manifest, manifest]
    if (!is.na(var_value) && var_value != 0) {
      line <- paste0(manifest, " ~~ ", round(var_value, 3), "*", manifest)
      model_string <- c(model_string, line)
    }
  }

  # 3. Latent variances
  for (latent in latents) {
    var_value <- S[latent, latent]
    if (!is.na(var_value) && var_value != 0) {
      line <- paste0(latent, " ~~ ", round(var_value, 3), "*", latent)
      model_string <- c(model_string, line)
    }
  }


  # Add group label if applicable
  if (!is.null(group_prefix)) {
    model_string <- c(paste0("# Group: ", group_prefix), model_string)
  }

  return(paste(model_string, collapse = "\n"))
}

#' Convert Mplus model to lavaan object
#'
#' Converts an Mplus model object to a fitted lavaan model. Automatically detects
#' model type (CFA, SEM, growth, path analysis) and uses appropriate lavaan function.
#'
#' @param mplus_object An Mplus model object containing model syntax and data
#' @param data Optional data frame to use instead of data from Mplus object
#'
#' @return A fitted lavaan model object
#' @importFrom lavaan cfa sem growth
#' @keywords internal
#' @noRd
convert_mplus_to_lavaan <- function(mplus_object, data = NULL) {
  if (!is.null(mplus_object$MODEL)) {
    mplus_syntax <- mplus_object$MODEL
  } else if (!is.null(mplus_object$model)) {
    mplus_syntax <- mplus_object$model
  } else {
    stop("Could not find model syntax in Mplus object")
  }

  if (is.null(data)) {
    if (!is.null(mplus_object$rdata)) {
      data <- mplus_object$rdata
    } else if (!is.null(mplus_object$data)) {
      data <- mplus_object$data
    } else {
      stop("No data provided and no data found in Mplus object")
    }
  }

  model_syntax <- extract_mplus_syntax(mplus_syntax)

  model_type <- detect_mplus_model_type(mplus_syntax)

  switch(model_type,
         "cfa" = {
           fit_lav <- lavaan::cfa(model_syntax, data = data)
         },
         "growth" = {
           fit_lav <- lavaan::growth(model_syntax, data = data)
         },
         "sem" = {
           fit_lav <- lavaan::sem(model_syntax, data = data)
         },
         "path" = {
           fit_lav <- lavaan::sem(model_syntax, data = data)
         },
         {
           fit_lav <- try_auto_detect_model(model_syntax, data, mplus_syntax)
         }
  )

  return(fit_lav)
}


#' Detect Mplus model type from syntax
#'
#' Analyzes Mplus model syntax to determine the appropriate lavaan model type
#' (CFA, SEM, growth, or path analysis).
#'
#' @param mplus_syntax Character string containing Mplus model syntax
#'
#' @return Character string indicating model type: "cfa", "growth", "sem", or "path"
#' @keywords internal
#' @noRd
detect_mplus_model_type <- function(mplus_syntax) {
  mplus_upper <- toupper(mplus_syntax)

  if (grepl("\\|", mplus_syntax) || grepl("S\\s+BY|I\\s+BY", mplus_upper)) {
    return("growth")
  }

  if (grepl("BY", mplus_upper) && !grepl("ON", mplus_upper) && !grepl("WITH", mplus_upper)) {
    return("cfa")
  }

  if (grepl("BY", mplus_upper) && grepl("ON", mplus_upper)) {
    return("sem")
  }

  if (grepl("ON", mplus_upper) && !grepl("BY", mplus_upper)) {
    return("path")
  }

  # Default to SEM
  return("sem")
}


#' Auto-detect model type by trying multiple lavaan functions
#'
#' Attempts to fit the model using different lavaan functions (cfa, sem, growth)
#' and returns the first successful fit. Used as fallback when model type detection fails.
#'
#' @param model_syntax Character string containing lavaan model syntax
#' @param data Data frame containing the model data
#' @param mplus_syntax Original Mplus syntax (for error messages)
#'
#' @return A fitted lavaan model object
#' @importFrom lavaan cfa sem growth
#' @keywords internal
#' @noRd
try_auto_detect_model <- function(model_syntax, data, mplus_syntax) {
  models <- list(
    cfa = try(lavaan::cfa(model_syntax, data = data), silent = TRUE),
    sem = try(lavaan::sem(model_syntax, data = data), silent = TRUE),
    growth = try(lavaan::growth(model_syntax, data = data), silent = TRUE)
  )

  for (model_name in names(models)) {
    if (!inherits(models[[model_name]], "try-error")) {
      return(models[[model_name]])
    }
  }

  warning("Could not auto-detect model type, using sem() as default")
  return(lavaan::sem(model_syntax, data = data))
}

#' Extract and convert Mplus syntax to lavaan syntax
#'
#' Parses Mplus model syntax and converts it to lavaan-compatible syntax.
#' Handles factor loadings (BY), regressions (ON), covariances (WITH),
#' and intercepts.
#'
#' @param mplus_syntax Character string containing raw Mplus model syntax
#'
#' @return Character string containing converted lavaan model syntax
#' @keywords internal
#' @noRd
extract_mplus_syntax <- function(mplus_syntax) {
  mplus_syntax <- gsub("!.*", "", mplus_syntax)
  mplus_syntax <- gsub("\\s+", " ", mplus_syntax)
  mplus_syntax <- trimws(mplus_syntax)

  lines <- strsplit(mplus_syntax, ";")[[1]]
  lines <- trimws(lines)
  lines <- lines[lines != ""]

  lavaan_lines <- character()

  for (line in lines) {
    line_upper <- toupper(line)

    if (grepl("^(TITLE|DATA|VARIABLE|DEFINE|MONTECARLO|ANALYSIS|OUTPUT|SAVEDATA|PLOT)", line_upper)) {
      next
    }

    if (grepl("^MODEL:$", line_upper)) {
      next
    }

    line_clean <- gsub("\\*\\([^\\)]+\\)", "", line)
    line_clean <- gsub("\\*", "", line_clean)
    line_clean <- gsub("@\\d+", "", line_clean)  #

    if (grepl(" BY ", line_upper)) {
      parts <- strsplit(line_clean, "\\s+[Bb][Yy]\\s+")[[1]]
      if (length(parts) == 2) {
        factor <- trimws(parts[1])
        indicators <- strsplit(trimws(parts[2]), "\\s+")[[1]]
        indicators <- indicators[indicators != ""]
        lavaan_lines <- c(lavaan_lines, paste(factor, "=~", paste(indicators, collapse = " + ")))
      }
      next
    }

    # ON statements (regressions)
    if (grepl(" ON ", line_upper)) {
      parts <- strsplit(line_clean, "\\s+[Oo][Nn]\\s+")[[1]]
      if (length(parts) == 2) {
        outcome <- trimws(parts[1])
        predictors <- strsplit(trimws(parts[2]), "\\s+")[[1]]
        predictors <- predictors[predictors != ""]
        lavaan_lines <- c(lavaan_lines, paste(outcome, "~", paste(predictors, collapse = " + ")))
      }
      next
    }

    # WITH statements (covariances)
    if (grepl(" WITH ", line_upper)) {
      parts <- strsplit(line_clean, "\\s+[Ww][Ii][Tt][Hh]\\s+")[[1]]
      if (length(parts) == 2) {
        var1 <- trimws(parts[1])
        var2 <- trimws(parts[2])
        lavaan_lines <- c(lavaan_lines, paste(var1, "~~", var2))
      }
      next
    }

    # Intercepts
    if (grepl("^\\[.*\\]$", line)) {
      var_name <- gsub("\\[|\\]", "", line)
      var_name <- trimws(var_name)
      lavaan_lines <- c(lavaan_lines, paste(var_name, "~ 1"))
      next
    }

    # Handle means (alternative intercept syntax)
    if (grepl("^\\s*\\w+\\s*\\[", line)) {
      var_name <- gsub("\\[.*", "", line)
      var_name <- trimws(var_name)
      lavaan_lines <- c(lavaan_lines, paste(var_name, "~ 1"))
      next
    }
  }

  unique_lines <- unique(lavaan_lines)
  clean_syntax <- paste(unique_lines, collapse = "\n")
  clean_syntax <- gsub("^MODEL:\\s*", "", clean_syntax)

  return(clean_syntax)
}




#' Extract lavaan syntax from OpenMx model
#'
#' Converts an OpenMx MxRAMModel object to lavaan model syntax by extracting
#' parameters from A (asymmetric), S (symmetric), and M (means) matrices.
#' Handles factor loadings, regressions, covariances, and intercepts.
#'
#' @param mx_model An OpenMx MxRAMModel object
#'
#' @return Character string containing lavaan model syntax
#' @keywords internal
#' @noRd
extract_mx_syntax <- function(mx_model) {
  if (!inherits(mx_model, "MxRAMModel")) {
    stop("Input must be an MxRAMModel object")
  }

  A <- tryCatch(
    mx_model$A$values,  # FINAL estimates
    error = function(e) stop("Could not extract A matrix from MxRAMModel")
  )
  S <- tryCatch(
    mx_model$S$values,  # FINAL estimates
    error = function(e) stop("Could not extract S matrix from MxRAMModel")
  )
  M <- if (!is.null(mx_model$M)) mx_model$M$values else NULL

  latents <- tryCatch(
    mx_model$latentVars,
    error = function(e) character(0)  # Return empty if not found
  )

  manifests <- tryCatch(
    mx_model$manifestVars,
    error = function(e) character(0)  # Return empty if not found
  )

  if (length(manifests) == 0) {
    manifests <- setdiff(rownames(A), latents)
  }

  if (length(manifests) == 0) {
    manifests <- rownames(A)
  }

  if (length(manifests) == 0) stop("No manifest variables found in model")
  if (is.null(dim(A)) || is.null(dim(S))) stop("A or S matrices are not properly formed")

  lines <- character()
  all_vars <- c(manifests, latents)

  # 1. Handle models WITH latent variables (measurement model)
  if (length(latents) > 0) {
    # Factor loadings (latent -> manifest)
    for (latent in latents) {
      indicators <- character()

      for (manifest in manifests) {
        # Check if this path exists and get FINAL value
        value <- A[manifest, latent]
        if (!is.na(value) && value != 0) {
          # Include the FINAL parameter value in syntax
          indicators <- c(indicators, paste0(value, "*", manifest))
        }
      }

      if (length(indicators) > 0) {
        lines <- c(lines, paste(latent, "=~", paste(indicators, collapse = " + ")))
      }
    }

    # Structural paths (latent -> latent)
    for (lhs in latents) {
      predictors <- character()

      for (rhs in latents) {
        if (lhs != rhs) { # Avoid self-regression
          value <- A[lhs, rhs]
          if (!is.na(value) && value != 0) {
            # Include the FINAL parameter value in syntax
            predictors <- c(predictors, paste0(value, "*", rhs))
          }
        }
      }

      if (length(predictors) > 0) {
        lines <- c(lines, paste(lhs, "~", paste(predictors, collapse = " + ")))
      }
    }
  }

  # 2. Handle ALL regression paths (manifest -> manifest, manifest -> latent, latent -> manifest)
  for (lhs in all_vars) {
    predictors <- character()

    for (rhs in all_vars) {
      if (lhs != rhs) { # Avoid self-regression
        value <- A[lhs, rhs]
        if (!is.na(value) && value != 0) {
          # Include the FINAL parameter value in syntax
          predictors <- c(predictors, paste0(value, "*", rhs))
        }
      }
    }

    if (length(predictors) > 0) {
      lines <- c(lines, paste(lhs, "~", paste(predictors, collapse = " + ")))
    }
  }

  # 3. Variances and covariances (for ALL variables)
  for (i in 1:length(all_vars)) {
    for (j in i:length(all_vars)) {  # Include diagonal (i <= j)
      var1 <- all_vars[i]
      var2 <- all_vars[j]

      # Get FINAL covariance value
      value <- S[var1, var2]

      if (!is.na(value) && value != 0) {
        if (i == j) {
          # Variance
          lines <- c(lines, paste(var1, "~~", paste0(value, "*", var1)))
        } else {
          # Covariance
          lines <- c(lines, paste(var1, "~~", paste0(value, "*", var2)))
        }
      }
    }
  }

  # 4. Means/intercepts
  if (!is.null(M)) {
    for (var in all_vars) {
      value <- M[1, var]
      if (!is.na(value) && value != 0) {
        # Include the FINAL intercept value in syntax
        lines <- c(lines, paste(var, "~", paste0(value, "*1")))
      }
    }
  }

  # Remove any duplicates and return
  unique_lines <- unique(lines)

  # If no lines were generated (empty model), return empty string
  if (length(unique_lines) == 0) {
    return("")
  }

  return(paste(unique_lines, collapse = "\n"))
}

#' Detect model type from OpenMx model
#'
#' Analyzes the structure of an OpenMx MxRAMModel to determine the appropriate
#' model type (CFA, SEM, growth, path analysis, EFA, etc.) based on matrix
#' patterns and variable relationships.
#'
#' @param mx_model An OpenMx MxRAMModel object
#'
#' @return Character string indicating model type: "cfa", "efa", "esem",
#'   "growth", "sem", "path", "multigroup", or "correlation"
#' @keywords internal
#' @noRd
detect_mx_model_type <- function(mx_model) {
  if (!inherits(mx_model, "MxRAMModel")) {
    stop("Input must be an MxRAMModel object")
  }

  A <- mx_model$A$values
  S <- mx_model$S$values
  latents <- mx_model$latentVars
  manifests <- mx_model$manifestVars

  # Basic counts
  n_manifests <- length(manifests)
  n_latents <- length(latents)

  # Path counts
  measurement_paths <- sum(A[manifests, latents] != 0 | !is.na(mx_model$A$free[manifests, latents]))
  structural_paths <- sum(A[latents, latents] != 0 | !is.na(mx_model$A$free[latents, latents]))
  manifest_regressions <- sum(A[manifests, manifests] != 0 | !is.na(mx_model$A$free[manifests, manifests]))

  # Check model characteristics
  has_means <- !is.null(mx_model$M)
  is_multigroup <- !is.null(mx_model$groups) && length(mx_model$groups) > 1

  # Growth model detection
  growth_indicators <- c("intercept", "slope", "linear", "quadratic", "growth")
  is_growth_latents <- any(grepl(paste(growth_indicators, collapse = "|"), latents, ignore.case = TRUE))

  time_indicators <- c("time", "age", "wave", "t[0-9]", "occasion", "session", "visit")
  has_time_vars <- any(grepl(paste(time_indicators, collapse = "|"),
                             c(latents, manifests), ignore.case = TRUE))

  # EFA vs CFA detection
  cross_loadings <- 0
  if (n_latents > 1 && n_manifests > 0) {
    loading_matrix <- A[manifests, latents] != 0 | !is.na(mx_model$A$free[manifests, latents])
    cross_loadings <- sum(rowSums(loading_matrix) > 1)
    cross_loading_ratio <- cross_loadings / n_manifests
  } else {
    cross_loading_ratio <- 0
  }

  # Decision tree
  if (n_latents == 0) {
    if (manifest_regressions > 0) return("path")
    else return("correlation")
  }
  else if (is_multigroup) {
    return("multigroup")
  }
  else if ((is_growth_latents || has_time_vars) && has_means) {
    return("growth")
  }
  else if (measurement_paths > 0 && structural_paths == 0 && manifest_regressions == 0) {
    if (cross_loading_ratio > 0.4) return("efa")
    else if (cross_loading_ratio > 0.1) return("esem")  # Exploratory SEM
    else return("cfa")
  }
  else if (measurement_paths > 0 && (structural_paths > 0 || manifest_regressions > 0)) {
    return("sem")
  }
  else if (structural_paths > measurement_paths) {
    return("path")
  }
  else {
    return("sem")  # Default
  }
}

#' Convert OpenMx model to fitted lavaan object
#'
#' Converts an OpenMx MxRAMModel to a fitted lavaan model by extracting the
#' syntax, detecting the model type, and using the appropriate lavaan fitting
#' function with provided data.
#'
#' @param mx_model An OpenMx MxRAMModel object
#' @param data Data frame containing the observed variables
#'
#' @return A fitted lavaan model object
#' @keywords internal
#' @noRd
convert_mx_to_lavaan <- function(mx_model, original_data = NULL) {
  if (!inherits(mx_model, "MxRAMModel")) {
    stop("Input must be an MxRAMModel object")
  }

  model_syntax <- extract_mx_syntax(mx_model)
  model_type <- detect_mx_model_type(mx_model)

  # Get data information from the OpenMx model
  mx_data <- mx_model$data

  if (is.null(mx_data)) {
    stop("No data found in the OpenMx model")
  }

  # Determine data type and extract appropriate data
  if (mx_data$type == "cov" || mx_data$type == "cor") {
    # Handle covariance/correlation matrix
    cov_matrix <- as.matrix(mx_data$observed)
    n_obs <- mx_data$numObs

    if (is.null(n_obs) || is.na(n_obs)) {
      # Try to extract N from the data if available
      n_obs <- tryCatch({
        if (!is.null(original_data) && is.data.frame(original_data)) {
          nrow(original_data)
        } else if (!is.null(mx_data$numObs)) {
          mx_data$numObs
        } else {
          NA
        }
      }, error = function(e) NA)
    }

    if (is.na(n_obs)) {
      warning("Number of observations not found, using N = 1000")
      n_obs <- 1000
    }

    # For covariance/correlation data
    switch(model_type,
           "cfa" = {
             fit_lav <- cfa(model_syntax, sample.cov = cov_matrix,
                            sample.nobs = n_obs)
           },
           "efa" = {
             fit_lav <- cfa(model_syntax, sample.cov = cov_matrix,
                            sample.nobs = n_obs, estimator = "ML")
           },
           "sem" = {
             fit_lav <- sem(model_syntax, sample.cov = cov_matrix,
                            sample.nobs = n_obs)
           },
           "path" = {
             fit_lav <- sem(model_syntax, sample.cov = cov_matrix,
                            sample.nobs = n_obs)
           },
           {
             # Default case for covariance data
             fit_lav <- sem(model_syntax, sample.cov = cov_matrix,
                            sample.nobs = n_obs)
           }
    )

  } else if (mx_data$type == "raw") {
    # Handle raw data
    raw_data <- as.data.frame(mx_data$observed)

    switch(model_type,
           "cfa" = {
             fit_lav <- cfa(model_syntax, data = raw_data)
           },
           "efa" = {
             fit_lav <- cfa(model_syntax, data = raw_data, estimator = "ML")
           },
           "growth" = {
             fit_lav <- growth(model_syntax, data = raw_data)
           },
           "sem" = {
             fit_lav <- sem(model_syntax, data = raw_data)
           },
           "path" = {
             fit_lav <- sem(model_syntax, data = raw_data)
           },
           {
             # Default case for raw data
             fit_lav <- sem(model_syntax, data = raw_data)
           }
    )
  } else {
    stop(sprintf("Unsupported data type: %s", mx_data$type))
  }

  return(fit_lav)
}

#' Convert OpenMx model to lavaan with fixed parameter estimates
#'
#' Internal function to convert an OpenMx RAM model to a lavaan model,
#' preserving parameter estimates as fixed values or starting values.
#' Automatically handles different data types (raw, covariance, correlation).
#'
#' @param mx_model An OpenMx MxRAMModel object to convert
#' @param data Optional data input. Can be: raw data frame, covariance matrix,
#'   correlation matrix, or the full OpenMx data object (mx_model$data).
#'   If NULL, data is extracted from mx_model$data$observed.
#'
#' @return A fitted lavaan model with parameters set to OpenMx estimates.
#'   Fixed parameters in OpenMx remain fixed in lavaan; free parameters
#'   use OpenMx estimates as starting values.
#'
#' @keywords internal
#' @noRd
convert_openmx_to_lavaan <- function(mx_model, data = NULL) {
  # Extract parameter estimates from OpenMx
  params <- get_openmx_parameters(mx_model)

  # Determine data type and source
  mx_data_info <- mx_model$data

  # Check if user passed the full OpenMx data object instead of just the data
  if (!is.null(data) && !is.null(data$observed)) {
    observed_data <- data$observed
    if (!is.null(data$type)) {
      data_type <- data$type
    } else {
      # Try to determine type
      if (is.matrix(observed_data) && nrow(observed_data) == ncol(observed_data)) {
        data_type <- "cov"
      } else if (is.data.frame(observed_data)) {
        data_type <- "raw"
      } else {
        stop("Unrecognized data format in data$observed")
      }
    }
  } else if (!is.null(data)) {
    # Data provided by user - determine type
    observed_data <- data
    if (is.matrix(data) && nrow(data) == ncol(data)) {
      data_type <- "cov"
    } else if (is.data.frame(data)) {
      data_type <- "raw"
    } else {
      stop("Unrecognized data format")
    }
  } else if (!is.null(mx_data_info) && !is.null(mx_data_info$observed)) {
    # Use data from OpenMx model
    observed_data <- mx_data_info$observed
    data_type <- mx_data_info$type  # "raw", "cov", or "cor"
  } else {
    stop("No data provided and cannot extract data from OpenMx object")
  }

  # Extract model syntax
  model_syntax <- extract_mx_syntax(mx_model)

  if (model_syntax == "" || nchar(model_syntax) == 0) {
    warning("Empty model syntax generated. Creating intercept-only model.")

    if (data_type == "raw") {
      var_names <- names(observed_data)
    } else {
      var_names <- colnames(observed_data)
    }

    if (is.null(var_names) || length(var_names) == 0) {
      stop("Cannot determine variable names from data")
    }

    intercept_lines <- paste(var_names, "~", "1")
    model_syntax <- paste(intercept_lines, collapse = "\n")

    params <- data.frame(
      lhs = var_names,
      op = "~1",
      rhs = "",
      est = rep(0, length(var_names)),
      se = rep(NA, length(var_names)),
      free = rep(1, length(var_names)),
      stringsAsFactors = FALSE
    )
  }

  lav_model <- create_lavaan_with_fixed_params(model_syntax, params, observed_data, data_type)

  return(lav_model)
}

#' Create lavaan model with parameters fixed to OpenMx estimates
#'
#' Internal function that takes OpenMx parameter estimates and applies them
#' as fixed values or starting values in a lavaan model. Handles covariance,
#' correlation, and raw data types.
#'
#' @param model_syntax Character string of lavaan model syntax
#' @param params Data frame of parameter estimates from OpenMx with columns:
#'   lhs, op, rhs, est, free (logical)
#' @param data Data for model fitting: data frame (raw), covariance matrix,
#'   or correlation matrix
#' @param data_type Optional: "raw", "cov", or "cor". Auto-detected if NULL
#'
#' @return A fitted lavaan model with parameters constrained to OpenMx estimates
#' @importFrom lavaan sem parTable lavaan
#' @keywords internal
#' @noRd
#'
#' @importFrom lavaan sem parTable lavaan
create_lavaan_with_fixed_params <- function(model_syntax, params, data, data_type = NULL) {
  # Auto-detect data type if not provided
  if (is.null(data_type)) {
    if (is.matrix(data) && nrow(data) == ncol(data)) {
      data_type <- "cov"
    } else {
      data_type <- "raw"
    }
  }

  # Parse the model to get parameter table
  if (data_type == "cov") {
    # For covariance data
    n_obs <- attr(data, "n_obs")
    if (is.null(n_obs)) {
      warning("Number of observations not specified for covariance matrix. Using N=1000.")
      n_obs <- 1000
    }

    # Initial lavaan model without fitting
    lav_model <- lavaan::sem(model_syntax,
                             sample.cov = data,
                             sample.nobs = n_obs,
                             do.fit = FALSE)

  } else if (data_type == "cor") {
    # For correlation data
    n_obs <- attr(data, "n_obs")
    if (is.null(n_obs)) {
      warning("Number of observations not specified for correlation matrix. Using N=1000.")
      n_obs <- 1000
    }

    lav_model <- lavaan::sem(model_syntax,
                             sample.cov = data,
                             sample.nobs = n_obs,
                             do.fit = FALSE)

  } else {
    # For raw data
    lav_model <- lavaan::sem(model_syntax,
                             data = data,
                             do.fit = FALSE)
  }

  # Get parameter table
  par_table <- lavaan::parTable(lav_model)

  # If params is a data frame (from get_openmx_parameters), process it
  if (is.data.frame(params) && nrow(params) > 0) {
    # Match parameters and set estimates and constraints
    for (i in 1:nrow(par_table)) {
      # Find matching parameter
      match_idx <- which(
        params$lhs == par_table$lhs[i] &
          params$op == par_table$op[i] &
          params$rhs == par_table$rhs[i]
      )

      if (length(match_idx) > 0) {
        # Set starting value to the OpenMx estimate
        par_table$ustart[i] <- params$est[match_idx[1]]

        # If parameter was fixed in OpenMx, fix it in lavaan
        if (!params$free[match_idx[1]]) {
          par_table$free[i] <- 0
        }
      }
    }
  }

  # Refit the model with updated parameter table
  if (data_type == "cov") {
    fit <- lavaan::lavaan(par_table,
                          sample.cov = data,
                          sample.nobs = n_obs,
                          fixed.x = FALSE,
                          estimator = "ML",
                          warn = FALSE)
  } else if (data_type == "cor") {
    fit <- lavaan::lavaan(par_table,
                          sample.cov = data,
                          sample.nobs = n_obs,
                          fixed.x = FALSE,
                          estimator = "ML",
                          warn = FALSE)
  } else {
    fit <- lavaan::lavaan(par_table,
                          data = data,
                          fixed.x = FALSE,
                          estimator = "ML",
                          warn = FALSE)
  }

  return(fit)
}


#' Extract lavaan syntax from fitted model
#'
#' Extracts the original lavaan model syntax from a fitted lavaan object.
#' Attempts to retrieve the syntax from the model call first, then falls back
#' to reconstruction from the parameter table if needed.
#'
#' @param fit A fitted lavaan object
#'
#' @return Character string containing the original lavaan model syntax
#' @keywords internal
#' @noRd
fit_to_lavstring <- function(fit) {
  if (!inherits(fit, 'lavaan')  && is(fit)[[1]] != "INLAvaan") {
    stop("Input must be a lavaan or INLAvaan object.")
  }

  # Try to extract from call first (preserves original formatting)
  model_string <- tryCatch({
    if (is.name(fit@call$model)) {
      # Case 1: Model stored in a variable
      eval(fit@call$model, envir = environment(fit@call))
    } else if (is.character(fit@call$model)) {
      # Case 2: Model passed directly as string
      as.character(fit@call$model)
    } else {
      # Case 3: Fall back to reconstruction
      stop("Cannot extract from call")
    }
  }, error = function(e) {
    # Fallback: reconstruct from parameter table (works with tidySEM)
    reconstruct_from_parTable(fit)
  })

  return(model_string)
}

#' Reconstruct lavaan syntax from parameter table
#'
#' Fallback method to reconstruct lavaan syntax from a fitted model's parameter
#' table when the original syntax cannot be extracted from the call. Handles
#' factor loadings, regressions, covariances, intercepts, and variances.
#'
#' @param fit A fitted lavaan object
#'
#' @return Character string containing reconstructed lavaan syntax
#' @keywords internal
#' @importFrom lavaan parTable
#' @noRd
reconstruct_from_parTable <- function(fit) {
  param_table <- lavaan::parTable(fit)
  lines <- character()

  # Get all observed variables to check for duplicates
  all_observed <- unique(c(
    param_table$rhs[param_table$op == "=~"],  # Indicators in measurement model
    param_table$lhs[param_table$op == "~"],   # Dependent variables in regressions
    param_table$rhs[param_table$op == "~"],   # Independent variables in regressions
    param_table$lhs[param_table$op == "~~" & param_table$lhs == param_table$rhs],  # Variances
    param_table$lhs[param_table$op == "~1"]   # Variables with intercepts
  ))

  # Remove empty strings and latent variables (those with =~ operator)
  latent_vars <- unique(param_table$lhs[param_table$op == "=~"])
  all_observed <- setdiff(all_observed, c("", latent_vars))

  # Check for duplicates
  if (any(duplicated(all_observed))) {
    warning("Duplicate observed variables found: ",
            paste(unique(all_observed[duplicated(all_observed)]), collapse = ", "))
  }

  # 1. Measurement model (==)
  loadings <- param_table[param_table$op == "=~", ]
  if (nrow(loadings) > 0) {
    factors <- unique(loadings$lhs)
    for (factor in factors) {
      indicators <- loadings$rhs[loadings$lhs == factor]
      indicators <- unique(indicators)
      lines <- c(lines, paste(factor, "=~", paste(indicators, collapse = " + ")))
    }
  }

  # 2. Structural model (~)
  regressions <- param_table[param_table$op == "~", ]
  if (nrow(regressions) > 0) {
    regressions <- regressions[!duplicated(regressions[, c("lhs", "rhs")]), ]
    for (i in 1:nrow(regressions)) {
      lines <- c(lines, paste(regressions$lhs[i], "~", regressions$rhs[i]))
    }
  }

  # 3. Covariances (~~) - with duplicate removal
  covariances <- param_table[param_table$op == "~~" & param_table$lhs != param_table$rhs, ]
  if (nrow(covariances) > 0) {
    unique_covs <- character()
    for (i in 1:nrow(covariances)) {
      pair <- sort(c(covariances$lhs[i], covariances$rhs[i]))
      pair_str <- paste(pair[1], "~~", pair[2])
      if (!pair_str %in% unique_covs) {
        unique_covs <- c(unique_covs, pair_str)
      }
    }
    lines <- c(lines, unique_covs)
  }

  # 4. Intercepts (~1)
  intercepts <- param_table[param_table$op == "~1" & !param_table$lhs %in% c("", ".p."), ]
  if (nrow(intercepts) > 0) {
    intercepts <- intercepts[!duplicated(intercepts$lhs), ]
    for (i in 1:nrow(intercepts)) {
      lines <- c(lines, paste(intercepts$lhs[i], "~ 1"))
    }
  }

  # 5. Variances (~~ with same variable) - only if user-specified or fixed
  variances <- param_table[param_table$op == "~~" & param_table$lhs == param_table$rhs, ]
  if (nrow(variances) > 0) {
    variances <- variances[!duplicated(variances$lhs), ]
    for (i in 1:nrow(variances)) {
      lines <- c(lines, paste(variances$lhs[i], "~~", variances$rhs[i]))
    }
  }

  # Remove any duplicate lines that might have been created
  lines <- unique(lines)

  return(paste(lines, collapse = "\n"))
}

#' Extract lavaan syntax from blavaan model
#'
#' Extracts lavaan syntax from a fitted blavaan (Bayesian lavaan) object
#' by reconstructing from the parameter table.
#'
#' @param fit A fitted blavaan object
#'
#' @return Character string containing the lavaan model syntax
#' @keywords internal
#' @noRd
blavaan_to_lavstring <- function(fit) {
  if (!inherits(fit, 'blavaan')) {
    stop("Input must be a blavaan object")
  }

  return(reconstruct_from_parTable(fit))
}


#' Convert blavaan model to semPaths with enhanced Bayesian options
#'
#'
#' @param fit Fitted blavaan model object or list of two blavaan models for comparison
#' @param data_file Optional data frame used for fitting the model
#' @param layout_algorithm Layout algorithm for semPaths (default: 'tree2')
#' @param intercepts Logical, whether to include intercepts in the visualization (default: TRUE)
#' @param annotate_estimates Logical, whether to annotate edges with parameter
#'   estimates (default: TRUE)
#' @param standardized Logical, whether to show standardized estimates (default: FALSE)
#' @param unstandardized Logical, whether to show unstandardized estimates (default: TRUE)
#' @param p_val Logical, whether to add asterisks for significant parameters based on
#'   HPD intervals (default: FALSE)
#' @param conf_int Logical, whether to show HPD credible intervals (default: FALSE)
#' @param multi_group Logical, whether the model is multi-group (default: FALSE)
#' @param group_var Character, name of grouping variable for multi-group models
#' @param group_level Character, specific group level to visualize in multi-group models
#' @param combine Logical, whether to combine two SEMs for comparison (default: FALSE)
#' @param group1 Character, group level of group1 for comparison
#' @param group2 Character, group level of group2 for comparison
#' @param sep_by Character, separator for combined estimates (default: " | ")
#' @param residuals Logical, show residuals or not
#'
#' @return A semPaths qgraph object for visualization with Bayesian parameter estimates
#'
#'
#' @importFrom blavaan blavInspect
#' @importFrom lavaan parameterEstimates standardizedSolution
#' @importFrom semPlot semPaths
#' @importFrom dplyr select mutate across filter left_join
#' @importFrom rlang .data
#' @importFrom methods is
#'
#' @keywords internal
#' @noRd
blavaan_to_sempaths <- function(fit, data_file = NULL, layout_algorithm = 'tree2',
                                intercepts = TRUE,
                                annotate_estimates = TRUE,
                                standardized = FALSE,
                                unstandardized = TRUE,
                                p_val = FALSE,
                                conf_int = FALSE,
                                multi_group = FALSE,
                                group_var = NULL,
                                group_level = NULL,
                                combine = FALSE,
                                group1 = NULL,
                                group2 = NULL,
                                sep_by = " | ",
                                residuals = FALSE) {

  if (is.list(fit)) {
    combine <- FALSE
    fake_combine <- TRUE
  } else {
    fake_combine <- FALSE
  }

  if (is.null(group1) || is.null(group2)) combine <- FALSE

  if (combine) {
    params <- get_comparison_table_bayes(fit = fit, rope = c(-0.1,0.1), group1 = group1, group2 = group2, sep_by = sep_by)

    group_info <- blavaan::blavInspect(fit, "group.label")

    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]
    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    if (!residuals) {
      if (length(self_loop_indices) > 0) {
        params1 <- edge_params[-self_loop_indices, ]
      } else {
        params1 <- edge_params
      }
    } else {
      params1 <- edge_params
    }

    if (!intercepts) {
      params1 <- params1[params1$op != "~1", ]
    }

    unstd <- params1$est  # Unstandardized comparison
    std <- params1$std   # Standardized comparison

    significant <- params1$significant
    significant[is.na(significant)] <- FALSE

    if (p_val == TRUE) {
      std[significant] <- paste0(std[significant], "*")
      unstd[significant] <- paste0(unstd[significant], "*")
    }

    ci_labels <- if (conf_int) {
      ci_col <- NULL
      if ("confint_unstd" %in% names(params1)) {
        ci_col <- params1$confint_unstd
      } else if ("credible_interval" %in% names(params1)) {
        ci_col <- params1$credible_interval
      } else if ("confint_combined" %in% names(params1)) {
        ci_col <- params1$confint_combined
      }
      if (!is.null(ci_col)) {
        ifelse(is.na(ci_col) | grepl("NA", ci_col), "", paste0("\n", ci_col))
      } else {
        ""
      }
    } else {
      ""
    }

    if (standardized == TRUE && unstandardized == TRUE) {
      base_labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      base_labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      base_labels <- unstd
    } else {
      base_labels <- NULL
    }

    edgeLabels <- if (conf_int && !is.null(base_labels)) {
      paste0(base_labels, ci_labels)
    } else {
      base_labels
    }

    sem_paths0 <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                    whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                    edge.color = ifelse(significant, "#000000", "#BEBEBE"))

    if (inherits(sem_paths0, 'list')) {
      sem_paths <- sem_paths0[[1]]
    } else if (inherits(sem_paths0, 'qgraph')) {
      sem_paths <- sem_paths0
    }

  } else if (fake_combine) {
    params <- combine_model_parameters_bayes(fit1 = fit[[1]], fit2 = fit[[2]], group1 = group1, group2 = group2, sep_by = sep_by)

    ref <- lavaan::parameterEstimates(fit[[1]])
    ref <- ref |> mutate(across(c(lhs, op, rhs), as.character))

    params <- ref |>
      select(lhs, op, rhs) |>
      left_join(params, by = c("lhs", "op", "rhs"))

    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]
    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    if (!residuals) {
      if (length(self_loop_indices) > 0) {
        params1 <- edge_params[-self_loop_indices, ]
      } else {
        params1 <- edge_params
      }
    } else {
      params1 <- edge_params
    }

    # Create fake significance column for compatibility
    params1$significant <- FALSE

    if (!intercepts) {
      params1 <- params1[params1$op != "~1", ]
    }

    unstd <- params1$est  # Unstandardized comparison
    std <- params1$std   # Standardized comparison

    significant <- params1$significant
    significant[is.na(significant)] <- FALSE

    if (p_val == TRUE) {
      std[significant] <- paste0(std[significant], "*")
      unstd[significant] <- paste0(unstd[significant], "*")
    }

    ci_labels <- if (conf_int) {
      ci_col <- NULL
      if ("confint_unstd" %in% names(params1)) {
        ci_col <- params1$confint_unstd
      } else if ("credible_interval" %in% names(params1)) {
        ci_col <- params1$credible_interval
      } else if ("confint_combined" %in% names(params1)) {
        ci_col <- params1$confint_combined
      }
      if (!is.null(ci_col)) {
        ifelse(is.na(ci_col) | grepl("NA", ci_col), "", paste0("\n", ci_col))
      } else {
        ""
      }
    } else {
      ""
    }

    if (standardized == TRUE && unstandardized == TRUE) {
      base_labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      base_labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      base_labels <- unstd
    } else {
      base_labels <- NULL
    }

    edgeLabels <- if (conf_int && !is.null(base_labels)) {
      paste0(base_labels, ci_labels)
    } else {
      base_labels
    }

    sem_paths <- semPlot::semPaths(fit[[1]], layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                   whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                   edge.color = ifelse(significant, "#000000", "#BEBEBE"))

  } else {
    params <- lavaan::parameterEstimates(fit)
    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]

    hpd_intervals <- as.data.frame(blavaan::blavInspect(fit, "hpd"))
    hpd_names <- rownames(hpd_intervals)

    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    if (multi_group == TRUE && !is.null(group_level)) {
      group_info <- blavaan::blavInspect(fit, "group.label")

      if (!is.null(group_level)) {
        group_number <- which(group_info == group_level)
        if (length(group_number) == 0) {
          stop("Group level '", group_level, "' not found. Available groups: ",
               paste(group_info, collapse = ", "))
        }
      } else {
        group_number <- 1
        group_level <- group_info[1]
      }

      is_blavaan_format <- any(grepl("^\\.p[0-9]+\\.$", hpd_names)) ||
        any(grepl("\\.\\.", hpd_names))

      if (!residuals) {
        if (length(self_loop_indices) > 0) {
          params1 <- edge_params[-self_loop_indices, ]
          std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
        } else {
          params1 <- edge_params
          std_est1 <- standardizedSolution(fit)
        }
      } else {
        params1 <- edge_params
        std_est1 <- standardizedSolution(fit)
      }

      params1 <- params1[params1$group == group_number, ]
      std_est1 <- std_est1[std_est1$group == group_number, ]

      if (is_blavaan_format) {
        if (group_number == 1) {
          # Group 1: parameters without .gX suffix or ..1 suffix
          group_hpd_indices <- which(!grepl("\\.g[0-9]+$", hpd_names) &
                                       !grepl("\\.\\.1$", hpd_names) &
                                       !grepl("\\.1\\.g[0-9]+$", hpd_names))
        } else {
          # Group 2+: parameters with .gX or ..1 suffix
          group_hpd_pattern <- paste0("(\\.g", group_number, "$|\\.\\.", group_number, "$)")
          group_hpd_indices <- grep(group_hpd_pattern, hpd_names)
        }

        if (length(group_hpd_indices) > 0) {
          hpd_intervals <- hpd_intervals[group_hpd_indices, ]
          hpd_names <- rownames(hpd_intervals)
        }
      }

    } else {
      # Single group case
      group_number <- 1

      if (!residuals) {
        if (length(self_loop_indices) > 0) {
          params1 <- edge_params[-self_loop_indices, ]
          std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
        } else {
          params1 <- edge_params
          std_est1 <- standardizedSolution(fit)
        }
      } else {
        params1 <- edge_params
        std_est1 <- standardizedSolution(fit)
      }
    }

    if (!intercepts) {
      params1 <- params1[params1$op != "~1", ]
      std_est1 <- std_est1[std_est1$op != "~1", ]
    }

    unstd <- round(params1$est, 2)  # Unstandardized
    std <- round(std_est1$est.std, 2)   # Standardized

    significant <- rep(FALSE, nrow(params1))
    ci_labels <- character(nrow(params1))

    if (!"label" %in% names(params1)) {
      params1$label <- NA_character_
    } else {
      params1$label <- as.character(params1$label)
    }

    for(i in 1:nrow(params1)) {
      hpd_name <- NULL
      if (multi_group == TRUE &&!is.null(group_level)) {
        if (!is.na(params1$label[i]) && params1$label[i] != "") {
          base_hpd_name <- params1$label[i]

          if ((multi_group == TRUE && !is.null(group_level)) &&
              any(grepl("^\\.p[0-9]+\\.$", hpd_names))) {

            if (group_number == 1) {
              hpd_name <- base_hpd_name
            } else {
              possible_names <- c(
                paste0(base_hpd_name, "..", group_number),
                paste0(base_hpd_name, ".g", group_number),
                base_hpd_name
              )

              for (possible_name in possible_names) {
                if (possible_name %in% hpd_names) {
                  hpd_name <- possible_name
                  break
                }
              }
            }
          } else {
            hpd_name <- base_hpd_name
          }

        } else if (params1$op[i] == "~~") {
          lhs <- params1$lhs[i]
          rhs <- params1$rhs[i]

          if (any(grepl("\\.\\.", hpd_names))) {
            base_hpd_name <- paste0(lhs, "..", rhs)

            if ((multi_group == TRUE && !is.null(group_level)) && group_number > 1) {
              # Try with .g2 suffix first
              possible_names <- c(
                paste0(base_hpd_name, ".g", group_number),
                base_hpd_name  # Also try without suffix
              )

              for (possible_name in possible_names) {
                if (possible_name %in% hpd_names) {
                  hpd_name <- possible_name
                  break
                }
              }
            } else {
              hpd_name <- base_hpd_name
            }
          } else {
            # Human-readable format: x1~~x1
            hpd_name <- paste0(lhs, "~~", rhs)
          }

        } else if (params1$op[i] == "~1") {
          if ((multi_group == TRUE && !is.null(group_level)) &&
              group_number > 1 &&
              params1$lhs[i] %in% c("visual", "textual", "speed")) {
            hpd_name <- paste0(params1$lhs[i], ".1.g", group_number)
          } else if (!is.na(params1$label[i]) && params1$label[i] != "") {
            base_hpd_name <- params1$label[i]

            if ((multi_group == TRUE && !is.null(group_level)) && group_number > 1) {
              possible_names <- c(
                paste0(base_hpd_name, "..", group_number),
                paste0(base_hpd_name, ".g", group_number),
                base_hpd_name
              )

              for (possible_name in possible_names) {
                if (possible_name %in% hpd_names) {
                  hpd_name <- possible_name
                  break
                }
              }
            } else {
              hpd_name <- base_hpd_name
            }
          }
        }
      }


      if (is.null(hpd_name) || !(hpd_name %in% hpd_names)) {
        human_readable_name <- paste0(params1$lhs[i], params1$op[i],
                                      ifelse(params1$op[i] == "~1", "", params1$rhs[i]))
        if (human_readable_name %in% hpd_names) {
          hpd_name <- human_readable_name
        }
      }

      if (!is.null(hpd_name) && hpd_name %in% hpd_names) {
        match_idx <- which(hpd_names == hpd_name)
        significant[i] <- hpd_intervals$lower[match_idx] > 0 | hpd_intervals$upper[match_idx] < 0
        ci_labels[i] <- paste0("[",
                               round(hpd_intervals$lower[match_idx], 2), ", ",
                               round(hpd_intervals$upper[match_idx], 2), "]")
      }
    }

    if (p_val == TRUE) {
      std[significant] <- paste0(std[significant], "*")
      unstd[significant] <- paste0(unstd[significant], "*")
    }

    if (standardized == TRUE && unstandardized == TRUE) {
      labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      labels <- unstd
    } else {
      labels <- NULL
    }

    edgeLabels <- switch(
      paste(standardized, unstandardized, conf_int, sep = "-"),
      "FALSE-TRUE-FALSE"  = unstd,
      "TRUE-FALSE-FALSE"  = std,
      "TRUE-TRUE-FALSE"   = labels,
      "FALSE-FALSE-FALSE" = labels,
      "FALSE-TRUE-TRUE"  = paste0(unstd, "\n", ci_labels),
      "TRUE-FALSE-TRUE"  = paste0(std, "\n",  ci_labels),
      "TRUE-TRUE-TRUE"   = paste0(labels, "\n",  ci_labels),
      "FALSE-FALSE-TRUE" = paste0(labels, ci_labels),
    )

    edge_colors <- ifelse(significant, "#000000", "#BEBEBE")

    if (multi_group == TRUE) {
      sem_paths0 <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                      whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                      edge.color = edge_colors)

      if (inherits(sem_paths0, 'list')) {
        names(sem_paths0) <- group_info
        sem_paths <- sem_paths0[[1]]
      } else {
        sem_paths <- sem_paths0
      }

    } else {
      if (!is.null(data_file)) {
        sem_paths <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                       whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = edge_colors)
      } else {
        sem_paths <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts,
                                       what = "paths", whatLabels = "par", edgeLabels = edgeLabels,
                                       residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = edge_colors)
      }
    }
  }

  return(sem_paths)
}

#' Convert lavaan model to semPaths with enhanced options
#'
#'
#' @param fit Fitted lavaan model object
#' @param data_file Optional data frame used for fitting the model
#' @param layout_algorithm Layout algorithm for semPaths (default: 'tree2')
#' @param intercepts Logical, whether to include intercepts in the visualization (default: TRUE)
#' @param annotate_estimates Logical, whether to annotate edges with parameter
#'   estimates (default: TRUE)
#' @param standardized Logical, whether to show standardized estimates (default: FALSE)
#' @param unstandardized Logical, whether to show unstandardized estimates (default: TRUE)
#' @param conf_int Logical, whether to show confidence intervals (default: FALSE)
#' @param p_val Logical, whether to add asterisks for significant parameters (default: FALSE)
#' @param multi_group Logical, whether the model is multi-group (default: FALSE)
#' @param group_var Character, name of grouping variable for multi-group models
#' @param group_level Character, specific group level to visualize in multi-group models
#' @param p_val_alpha alpha level of p-value significance, double
#' @param combine combine two SEMs, logical
#' @param group1 group level of group1, character
#' @param group2 group level of group2, character
#' @param sep_by character separation, no space
#' @param residuals incldue residuals or not, logical
#'
#' @return A semPaths qgraph object for visualization
#' @importFrom lavaan parameterEstimates standardizedSolution lavInspect
#' @importFrom semPlot semPaths
#' @importFrom dplyr select mutate across filter left_join
#' @importFrom rlang .data
#' @keywords internal
#' @noRd
lavaan_to_sempaths <- function(fit, data_file = NULL, layout_algorithm = 'tree2',
                               intercepts = TRUE,
                               annotate_estimates = TRUE,
                               standardized = FALSE,
                               unstandardized = TRUE,
                               conf_int = FALSE,
                               p_val = FALSE,
                               multi_group = FALSE,
                               group_var = NULL,
                               group_level = NULL,
                               p_val_alpha = 0.05,
                               combine = FALSE,
                               group1 = NULL,
                               group2 = NULL,
                               sep_by = " |",
                               residuals = FALSE) {

  if (is.list(fit)) {
    combine <- FALSE
    fake_combine <- TRUE
  } else {
    fake_combine <- FALSE
  }

  if (is.null(group1) || is.null(group2)) combine <- FALSE

  if (combine) {
    params <- get_comparison_table(fit = fit, alpha = p_val_alpha, group1 = group1, group2 = group2, sep_by = sep_by)

    group_info <- lavInspect(fit, "group.label")
    group_number <- which(group_info == group_level)

    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]
    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    if (!residuals) {
      if (length(self_loop_indices) > 0) {
        params1 <- edge_params[-self_loop_indices, ]
        std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
      } else {
        params1 <- edge_params
      }
    } else {
      params1 <- edge_params
      std_est1 <- standardizedSolution(fit)
    }

    if (!intercepts) params1 <- params1[params1$op != "~1", ]

    unstd <- params1$est  # Unstandardized
    std <- params1$std   # Standardized

    # params1$pvalue[is.na(params1$pvalue)] <- 1
    # std_est1$pvalue[is.na(std_est1$pvalue)] <- 1
    if (!"pvalue" %in% names(params1)) {
      params1$pvalue <- 1
    } else {
      params1$pvalue[is.na(params1$pvalue)] <- 1
    }
    if (!"pvalue" %in% names(std_est1)) {
      std_est1$pvalue <- 1
    } else {
      std_est1$pvalue[is.na(std_est1$pvalue)] <- 1
    }
    # pval_idx <- which(params1$pvalue < p_val_alpha)

    if (p_val == TRUE) {
      std[which(std_est1$pvalue < p_val_alpha)] <- paste0(std[which(std_est1$pvalue < p_val_alpha)], "*")
      unstd[which(params1$pvalue < p_val_alpha)] <- paste0(unstd[which(params1$pvalue < p_val_alpha)], "*")
    }

    ci_labels <- if (conf_int) {
      # Determine which CI to show
      ci_to_show <- if (standardized == TRUE && unstandardized == FALSE) {
        params1$confint_std
      } else {
        params1$confint_unstd
      }

      # Add \n only if we're also showing point estimates
      if (standardized == FALSE && unstandardized == FALSE) {
        ci_to_show  # No \n when only showing CI
      } else {
        paste0("\n", ci_to_show)  # Add \n when showing with point estimates
      }
    } else {
      ""
    }


    if (standardized == TRUE && unstandardized == TRUE) {
      base_labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      base_labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      base_labels <- unstd
    } else {
      base_labels <- NULL
    }

    edgeLabels <- if (conf_int && !is.null(base_labels)) {
      paste0(base_labels, ci_labels)
    } else {
      base_labels
    }

    if (standardized == TRUE && unstandardized == FALSE) {
      edge_colors <- ifelse(std_est1$pvalue < p_val_alpha, "#000000", "#BEBEBE")
    } else {
      edge_colors <- ifelse(params1$pvalue < p_val_alpha, "#000000", "#BEBEBE")
    }


    sem_paths0 <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                    whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                    edge.color = edge_colors)


    if (inherits(sem_paths0, 'list')) {
      sem_paths <- sem_paths0[[1]]
    } else if (is(sem_paths0) == 'qgraph') {
      sem_paths <- sem_paths0
    }


  } else if (fake_combine) {

    params <- combine_model_parameters(fit1 = fit[[1]], fit2 = fit[[2]], group1 = group1, group2 = group2, sep_by = sep_by)

    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]
    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    if (!residuals) {
      if (length(self_loop_indices) > 0) {
        params1 <- edge_params[-self_loop_indices, ]
        # std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
      } else {
        params1 <- edge_params
      }
    } else {
      params1 <- edge_params
      # std_est1 <- standardizedSolution(fit)
    }

    params1$pvalue <- 1 # fake column

    if (!intercepts) params1 <- params1[params1$op != "~1", ]

    unstd <- params1$est  # Unstandardized
    std <- params1$std   # Standardized

    ci_labels <- if (conf_int) {
      if (standardized == FALSE && unstandardized == FALSE) {
        params1$confint_unstd
      } else if (standardized == TRUE && unstandardized == TRUE) {
        paste0("\n", params1$confint_unstd)
      } else if (standardized == TRUE && unstandardized == FALSE) {
        paste0("\n", params1$confint_std)
      } else {
        paste0("\n", params1$confint_unstd)
      }
    } else {
      ""
    }
    if (standardized == TRUE && unstandardized == TRUE) {
      base_labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      base_labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      base_labels <- unstd
    } else {
      base_labels <- NULL
    }


    edgeLabels <- if (conf_int && !is.null(base_labels)) {
      paste0(base_labels, ci_labels)
    } else {
      base_labels
    }

    if (multi_group == TRUE) {
      sem_paths0 <- semPlot::semPaths(fit[[1]], layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                      whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                      edge.color = ifelse(params1$pvalue < p_val_alpha, "#000000", "#BEBEBE"))

      names(sem_paths0) <- group_info
      sem_paths <- sem_paths0[[1]]

    } else if (multi_group == FALSE) {
      if (!is.null(data_file)) {
        sem_paths <- semPlot::semPaths(fit[[1]], layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                       whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = ifelse(params1$pvalue < p_val_alpha, "#000000", "#BEBEBE"))
      } else {
        sem_paths <- semPlot::semPaths(fit[[1]], layout = layout_algorithm, intercepts = intercepts,
                                       what = "paths", whatLabels = "par", edgeLabels = edgeLabels,
                                       residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = ifelse(params1$pvalue < p_val_alpha, "#000000", "#BEBEBE"))
      }
    }

  } else {
    params <- lavaan::parameterEstimates(fit)
    edge_params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]

    self_loop_indices <- which(edge_params$lhs == edge_params$rhs)

    # Filter parameters based on multi-group setting
    if (multi_group == TRUE && !is.null(group_level)) {
      # Get the group number for the specified group level
      group_info <- lavInspect(fit, "group.label")
      group_number <- which(group_info == group_level)

      if (length(group_number) == 0) {
        stop("Group level '", group_level, "' not found in the model. Available groups: ",
             paste(group_info, collapse = ", "))
      }

      if (!residuals) {
        if (length(self_loop_indices) > 0) {
          params1 <- edge_params[-self_loop_indices, ]
          std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
        } else {
          params1 <- edge_params
          std_est1 <- standardizedSolution(fit)
        }
      } else {
        params1 <- edge_params
        std_est1 <- standardizedSolution(fit)
      }

      # Filter parameters for the specific group
      params1 <- params1[params1$group == group_number, ]
      std_est1 <- std_est1[std_est1$group == group_number, ]

    } else {
      # Single group case
      if (!residuals) {
        if (length(self_loop_indices) > 0) {
          params1 <- edge_params[-self_loop_indices, ]
          std_est1 <- standardizedSolution(fit)[-self_loop_indices, ]
        } else {
          params1 <- edge_params
          std_est1 <- standardizedSolution(fit)
        }
      } else {
        params1 <- edge_params
        std_est1 <- standardizedSolution(fit)
      }
    }

    if (!intercepts) {
      params1 <- params1[params1$op != "~1", ]
      std_est1 <- std_est1[std_est1$op != "~1", ]
    }

    unstd <- round(params1$est, 2)  # Unstandardized
    std <- round(std_est1$est.std, 2)   # Standardized

    # Handle NA p-values
    # params1$pvalue[is.na(params1$pvalue)] <- 1
    # std_est1$pvalue[is.na(std_est1$pvalue)] <- 1
    if (!"pvalue" %in% names(params1)) {
      params1$pvalue <- 1
    } else {
      params1$pvalue[is.na(params1$pvalue)] <- 1
    }
    if (!"pvalue" %in% names(std_est1)) {
      std_est1$pvalue <- 1
    } else {
      std_est1$pvalue[is.na(std_est1$pvalue)] <- 1
    }

    # Apply significance stars based on which values are shown
    if (p_val == TRUE) {
      std[which(std_est1$pvalue < p_val_alpha)] <- paste0(std[which(std_est1$pvalue < p_val_alpha)], "*")
      unstd[which(params1$pvalue < p_val_alpha)] <- paste0(unstd[which(params1$pvalue < p_val_alpha)], "*")
    }

    if (standardized == TRUE && unstandardized == TRUE) {
      labels <- paste0(unstd, " (", std, ")")
    } else if (standardized == TRUE && unstandardized == FALSE) {
      labels <- std
    } else if (standardized == FALSE && unstandardized == TRUE) {
      labels <- unstd
    } else {
      labels <- NULL
    }

    if (conf_int) {
      if (standardized == TRUE && unstandardized == FALSE) {
        if (standardized == FALSE && unstandardized == FALSE) {
          ci_labels <- paste0("[",
                              round(std_est1$ci.lower, 2), ", ",
                              round(std_est1$ci.upper, 2), "]")
        } else {
          ci_labels <- paste0("\n[",
                              round(std_est1$ci.lower, 2), ", ",
                              round(std_est1$ci.upper, 2), "]")
        }
      } else {
        if (standardized == FALSE && unstandardized == FALSE) {
          ci_labels <- paste0("[",
                              round(params1$ci.lower, 2), ", ",
                              round(params1$ci.upper, 2), "]")
        } else {
          ci_labels <- paste0("\n[",
                              round(params1$ci.lower, 2), ", ",
                              round(params1$ci.upper, 2), "]")
        }
      }
    } else {
      ci_labels <- ""
    }

    edgeLabels <- switch(
      paste(standardized, unstandardized, conf_int, sep = "-"),
      "FALSE-TRUE-FALSE"  = unstd,
      "TRUE-FALSE-FALSE"  = std,
      "TRUE-TRUE-FALSE"   = labels,
      "FALSE-FALSE-FALSE" = labels,
      "FALSE-TRUE-TRUE"  = paste0(unstd, ci_labels),
      "TRUE-FALSE-TRUE"  = paste0(std, ci_labels),
      "TRUE-TRUE-TRUE"   = paste0(labels, ci_labels),
      "FALSE-FALSE-TRUE" = paste0(labels, ci_labels),
    )

    if (standardized == TRUE && unstandardized == FALSE) {
      edge_colors <- ifelse(std_est1$pvalue < p_val_alpha, "#000000", "#BEBEBE")
    } else {
      edge_colors <- ifelse(params1$pvalue < p_val_alpha, "#000000", "#BEBEBE")
    }

    if (multi_group == TRUE) {
      sem_paths0 <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                      whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals,
                                      plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                      edge.color = edge_colors)

      names(sem_paths0) <- group_info
      sem_paths <- sem_paths0[[1]]

    } else if (multi_group == FALSE) {
      if (!is.null(data_file)) {
        sem_paths <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts, what = "paths",
                                       whatLabels = "par", edgeLabels = edgeLabels, residuals = residuals,
                                       plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = edge_colors)
      } else {
        sem_paths <- semPlot::semPaths(fit, layout = layout_algorithm, intercepts = intercepts,
                                       what = "paths", whatLabels = "par", edgeLabels = edgeLabels,
                                       residuals = residuals, plot = FALSE, title = FALSE, DoNotPlot = TRUE,
                                       edge.color = edge_colors)
      }
    }

  }

  return(sem_paths)
}

#' Convert lavaan syntax to fitted model
#'
#' Fits structural equation models using lavaan syntax with support for custom
#' model fitting code, multi-group analysis, and various model types including
#' CFA, SEM, growth models, and EFA.
#'
#' @param lavaan_string Character string containing lavaan model syntax
#' @param sem_code Character string containing custom SEM fitting code (optional).
#'   If provided, this code will be executed instead of standard lavaan functions.
#'   Must include `lavaan_string` and `data` variables.
#' @param data_file Data frame containing the observed variables for model fitting
#' @param multi_group Logical, whether to perform multi-group analysis (default: FALSE)
#' @param group_var Character, name of grouping variable for multi-group analysis
#' @param invariance_level Character, level of measurement invariance for multi-group
#'   analysis: "configural", "metric", "scalar", or "strict" (default: NULL)
#' @param custom_sem Logical, whether to use custom SEM code when provided (default: TRUE)
#' @param model_type Character, type of model to fit: "sem", "cfa", "growth", or "efa"
#'   (default: "sem")
#' @param nfactors Integer, number of factors for EFA models (required for EFA,
#'   default: NULL)
#'
#' @return A fitted lavaan model object
#'
#' @details
#' This function provides a flexible interface for fitting SEM models with several
#' key features:
#'
#' \strong{Model Type Support:}
#' \itemize{
#'   \item \strong{SEM}: Structural Equation Modeling with latent variables
#'   \item \strong{CFA}: Confirmatory Factor Analysis
#'   \item \strong{Growth}: Latent growth curve models
#'   \item \strong{EFA}: Exploratory Factor Analysis (requires nfactors parameter)
#' }
#'
#' \strong{Multi-Group Analysis:}
#' \itemize{
#'   \item Configural invariance (no constraints)
#'   \item Metric invariance (equal loadings)
#'   \item Scalar invariance (equal loadings and intercepts)
#'   \item Strict invariance (equal loadings, intercepts, and residuals)
#' }
#'
#' \strong{Custom SEM Code:}
#' When `sem_code` is provided and `custom_sem = TRUE`, the function evaluates
#' the custom code instead of using standard lavaan functions. The custom code must:
#' \itemize{
#'   \item Include `lavaan_string` and `data` variables
#'   \item Use valid lavaan functions (sem, cfa, growth, efa, lavaan)
#'   \item Return a fitted lavaan model object
#' }
#'
#' \strong{Error Handling:}
#' The function includes comprehensive error checking for:
#' \itemize{
#'   \item Missing data files
#'   \item Invalid custom code syntax
#'   \item Missing required parameters (group_var for multi-group, nfactors for EFA)
#'   \item Model fitting errors with informative messages
#' }
#'
#' @examples
#' \dontrun{
#' # Basic SEM
#' model_syntax <- "
#'   # Measurement model
#'   visual =~ x1 + x2 + x3
#'   textual =~ x4 + x5 + x6
#'   speed =~ x7 + x8 + x9
#'
#'   # Structural model
#'   visual ~ textual + speed
#' "
#' fit <- lavstring_to_fit(model_syntax, data_file = HolzingerSwineford1939)
#'
#' # Multi-group CFA with metric invariance
#' fit <- lavstring_to_fit(
#'   model_syntax,
#'   data_file = data,
#'   multi_group = TRUE,
#'   group_var = "school",
#'   invariance_level = "metric",
#'   model_type = "cfa"
#' )
#'
#' # Custom SEM code
#' custom_code <- "cfa(lavaan_string, data = data, estimator = 'MLR')"
#' fit <- lavstring_to_fit(model_syntax, data_file = data, sem_code = custom_code)
#'
#' # EFA with 3 factors
#' efa_syntax <- "f1 =~ x1 + x2 + x3 + x4 + x5"
#' fit <- lavstring_to_fit(efa_syntax, data_file = data, model_type = "efa", nfactors = 3)
#' }
#'
#' @importFrom lavaan lavaanify sem cfa growth efa
#' @keywords internal
#' @noRd
lavstring_to_fit <- function(lavaan_string, sem_code = NULL, data_file = NULL, multi_group = FALSE,
                             group_var = NULL, invariance_level = NULL, custom_sem = TRUE,
                             model_type = "sem", nfactors = NULL) {

  if (multi_group) custom_sem = FALSE

  if (is.null(data_file)) {
    stop("Data file must be provided")
  }

  model <- lavaan::lavaanify(lavaan_string)
  latent_vars <- unique(model$lhs[model$op == "=~"])    # Latent variables
  observed_vars <- unique(setdiff(model$rhs[model$op %in% c("=~", "~", "~~")], model$lhs[model$op == "=~"]))

  data <- data_file

  if (!is.null(sem_code) && custom_sem) {
    # Check for required components in custom code
    if (!grepl("lavaan_string", sem_code) || !grepl("data", sem_code)) {
      stop("Custom SEM code must include `lavaan_string` and `data`.")
    }

    allowed_functions <- c("sem", "cfa", "growth", "efa", "lavaan")
    function_found <- sapply(allowed_functions, function(fun) grepl(paste0("\\b", fun, "\\("), sem_code))

    if (!any(function_found)) {
      stop("Custom SEM code must use a valid lavaan function: ", paste(allowed_functions, collapse = ", "))
    }
  }

  fit <- tryCatch({
    if (custom_sem && !is.null(sem_code)) {
      # Use custom SEM code
      eval(parse(text = sem_code))
    } else {
      if (multi_group) {
        if (is.null(group_var)) {
          stop("group_var must be specified for multi-group analysis")
        }

        group_equal_args <- switch(invariance_level,
                                   "configural" = NULL,
                                   "metric" = "loadings",
                                   "scalar" = c("loadings", "intercepts"),
                                   "strict" = c("loadings", "intercepts", "residuals"),
                                   NULL)

        # Choose appropriate function based on model type
        switch(model_type,
               "sem" = sem(lavaan_string, data = data, group = group_var, group.equal = group_equal_args),
               "cfa" = cfa(lavaan_string, data = data, group = group_var, group.equal = group_equal_args),
               "growth" = growth(lavaan_string, data = data, group = group_var, group.equal = group_equal_args),
               "efa" = {
                 warning("EFA typically doesn't support multi-group analysis. Using CFA instead.")
                 cfa(lavaan_string, data = data, group = group_var, group.equal = group_equal_args)
               },
               # Default to sem
               sem(lavaan_string, data = data, group = group_var, group.equal = group_equal_args)
        )
      } else {
        # Single-group analysis
        switch(model_type,
               "sem" = sem(lavaan_string, data = data),
               "cfa" = cfa(lavaan_string, data = data),
               "growth" = growth(lavaan_string, data = data),
               "efa" = {
                 if (is.null(nfactors)) {
                   stop("nfactors must be specified for EFA models")
                 }
                 efa(lavaan_string, data = data, nfactors = nfactors)
               },
               sem(lavaan_string, data = data)
        )
      }
    }
  }, error = function(e) {
    stop("Error in SEM model fitting: ", e$message)
  })

  return(fit)
}

#' Extract Parameters from OpenMx Model
#'
#' This internal function extracts parameter estimates from an OpenMx model object
#' and converts them into a standardized data frame format similar to lavaan's
#' parameter table structure.
#'
#' @param fit An OpenMx model object from which to extract parameters.
#'
#' @return A data frame with the following columns:
#' \itemize{
#'   \item \code{lhs}: Left-hand side variable name
#'   \item \code{op}: Operator symbol ("~" for regressions, "~~" for covariances, "~1" for means)
#'   \item \code{rhs}: Right-hand side variable name (empty for means/intercepts)
#'   \item \code{est}: Parameter estimate value
#'   \item \code{free}: Logical indicating whether the parameter is free (TRUE) or fixed (FALSE)
#'   \item \code{group}: Group identifier (always 1 for single-group models)
#' }
#'
#' @details
#' The function processes three main OpenMx matrices:
#' \itemize{
#'   \item \code{A} matrix: Contains regression coefficients (operator "~")
#'   \item \code{S} matrix: Contains variances and covariances (operator "~~")
#'   \item \code{M} matrix: Contains means and intercepts (operator "~1")
#' }
#'
#' For covariance matrices (\code{S}), the function avoids duplicate entries by
#' only including the lower triangle (excluding the diagonal for covariances).
#' The function extracts both free and fixed parameters that have non-zero values.
#'
#' @note
#' This function is designed for internal use within the package to convert
#' OpenMx model parameters into a standardized format compatible with other
#' SEM visualization functions. It currently supports single-group models only.
#'
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming mx_fit is a fitted OpenMx model
#' params <- get_openmx_parameters(mx_fit)
#' }
#'
get_openmx_parameters <- function(fit) {
  params <- data.frame(
    lhs = character(),
    op = character(),
    rhs = character(),
    est = numeric(),
    free = logical(),
    stringsAsFactors = FALSE
  )

  add_params <- function(mat, op_symbol) {
    if (!mat %in% names(fit$matrices)) return()

    mat_values <- fit[[mat]]$values
    mat_labels <- fit[[mat]]$labels
    mat_free <- fit[[mat]]$free

    non_zero <- which(mat_values != 0 | mat_free, arr.ind = TRUE)

    if (length(non_zero) > 0) {
      row_names <- rownames(mat_values)
      col_names <- colnames(mat_values)

      for (i in 1:nrow(non_zero)) {
        row_idx <- non_zero[i, 1]  # Fixed: row index first
        col_idx <- non_zero[i, 2]  # Fixed: column index second

        # Skip symmetric duplicates for covariance matrices
        if (mat == "S" && row_idx != col_idx && row_idx > col_idx) next

        # For means/intercepts, adjust indexing
        if (mat == "M") {
          lhs <- col_names[col_idx]
          rhs <- ""
        } else {
          lhs <- row_names[row_idx]
          rhs <- col_names[col_idx]
        }

        # Determine if parameter is free
        is_free <- isTRUE(mat_free[row_idx, col_idx])

        new_row <- data.frame(
          lhs = lhs,
          op = op_symbol,
          rhs = rhs,
          est = mat_values[row_idx, col_idx],
          free = is_free,
          stringsAsFactors = FALSE
        )
        params <<- rbind(params, new_row)
      }
    }
  }

  # Process matrices
  add_params("A", "~")   # Regressions
  add_params("S", "~~")  # Variances/covariances
  add_params("M", "~1")  # Means/intercepts

  # Add group information
  if (nrow(params) > 0) {
    params$group <- 1
  }

  return(params)
}

#' Combine tidySEM Groups for Multi-Group Analysis
#'
#' This internal function combines parameter estimates from multiple groups within
#' a single tidySEM object. It merges estimates, confidence intervals, and
#' significance indicators for specified groups into a unified visualization object
#' for frequentist multi-group SEM models.
#'
#' @param tidysem_obj A tidySEM graph object containing multiple groups.
#' @param group1 Character string specifying the name of the first group to compare.
#' @param group2 Character string specifying the name of the second group to compare.
#' @param sep_by Character string used to separate group values in the output
#'   (default: " | ").
#' @param standardized Logical indicating whether to include standardized estimates
#'   in the output (default: FALSE).
#' @param unstandardized Logical indicating whether to include unstandardized estimates
#'   in the output (default: TRUE).
#' @param p_val Logical indicating whether to include significance indicators
#'   in the output (default: TRUE).
#' @param conf_int Logical indicating whether to include confidence intervals
#'   in the output (default: FALSE).
#'
#' @return A modified tidySEM object with combined group information
#'
#'
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming tidysem_obj contains multiple groups
#' combined <- combine_tidysem_groups(
#'   tidysem_obj,
#'   group1 = "Group1", group2 = "Group2",
#'   standardized = TRUE, unstandardized = TRUE,
#'   p_val = TRUE, conf_int = TRUE
#' )
#' }
#'
#'
#' @importFrom dplyr mutate across group_by summarise filter arrange select distinct first left_join case_when row_number
#' @importFrom stats na.omit
#' @importFrom rlang .data
combine_tidysem_groups <- function(tidysem_obj, group1 = "", group2 = "",
                                   sep_by = " | ", standardized = FALSE, unstandardized = TRUE,
                                   p_val = TRUE, conf_int = FALSE) {

  # Ensure numeric columns are actually numeric
  tidysem_obj$edges <- tidysem_obj$edges |>
    mutate(across(c(est, est_std), ~ as.numeric(as.character(.x))))

  tidysem_obj$nodes <- tidysem_obj$nodes |>
    mutate(across(c(est, est_std), ~ as.numeric(as.character(.x))))

  original_edges_order <- tidysem_obj$edges |>
    distinct(lhs, op, rhs) |>
    mutate(original_order = row_number())

  # Combine edges
  edges_combined <- tidysem_obj$edges |>
    group_by(lhs, op, rhs) |>
    summarise(
      est_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(est[group == group1]), "NA", round(est[group == group1], 2)),
          ifelse(is.na(est[group == group2]), "NA", round(est[group == group2], 2)),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      est_sig_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(est_sig[group == group1]), "NA", est_sig[group == group1]),
          ifelse(is.na(est_sig[group == group2]), "NA", est_sig[group == group2]),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      # Add standardized estimates
      est_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(est_std[group == group1]), "NA", round(est_std[group == group1], 2)),
          ifelse(is.na(est_std[group == group2]), "NA", round(est_std[group == group2], 2)),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      est_sig_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(est_sig_std[group == group1]), "NA", est_sig_std[group == group1]),
          ifelse(is.na(est_sig_std[group == group2]), "NA", est_sig_std[group == group2]),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      # Add confidence intervals
      confint_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(confint[group == group1]), "NA", confint[group == group1]),
          ifelse(is.na(confint[group == group2]), "NA", confint[group == group2]),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      confint_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(
          ifelse(is.na(confint_std[group == group1]), "NA", confint_std[group == group1]),
          ifelse(is.na(confint_std[group == group2]), "NA", confint_std[group == group2]),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      across(c(from, to, arrow, connect_from, connect_to, curvature,
               linetype, block, show, label_results), first),
      .groups = 'drop'
    ) |>
    filter(!is.na(est_combined)) |>
    mutate(
      across(c(est_combined, est_sig_combined, est_std_combined, est_sig_std_combined,
               confint_combined, confint_std_combined),
             ~ ifelse(.x == paste0("NA", sep_by, "NA"), "", .x))
    ) |>
    mutate(
      label = case_when(
        # Case 1: Both standardized and unstandardized are FALSE
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint_combined
          } else {
            ""
          },

        # Case 2: Both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")")
            }
          } else {
            if (conf_int) {
              paste0(est_combined, " (", est_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_combined, " (", est_std_combined, ")")
            }
          },

        # Case 3: Only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_std_combined, "\n", confint_std_combined)
            } else {
              est_sig_std_combined
            }
          } else {
            if (conf_int) {
              paste0(est_std_combined, "\n", confint_std_combined)
            } else {
              est_std_combined
            }
          },

        # Case 4: Only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, "\n", confint_combined)
            } else {
              est_sig_combined
            }
          } else {
            if (conf_int) {
              paste0(est_combined, "\n", confint_combined)
            } else {
              est_combined
            }
          },

        TRUE ~ ""  # Fallback (shouldn't be reached)
      )
    ) |>
    # Reorder to match original order
    left_join(original_edges_order, by = c("lhs", "op", "rhs")) |>
    arrange(original_order) |>
    select(-original_order)

  original_nodes_order <- tidysem_obj$nodes |>
    distinct(name, op, rhs) |>
    mutate(original_order = row_number())

  # Combine nodes
  nodes_combined <- tidysem_obj$nodes |>
    group_by(name, op, rhs) |>
    summarise(
      est_combined = if (all(c(group1, group2) %in% group)) {
        paste(round(est[group == group1], 2),
              round(est[group == group2], 2), sep = sep_by)
      } else {
        NA_character_
      },
      est_sig_combined = if (all(c(group1, group2) %in% group)) {
        paste(est_sig[group == group1],
              est_sig[group == group2], sep = sep_by)
      } else {
        NA_character_
      },
      # Add standardized estimates for nodes
      est_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(round(est_std[group == group1], 2),
              round(est_std[group == group2], 2), sep = sep_by)
      } else {
        NA_character_
      },
      est_sig_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(est_sig_std[group == group1],
              est_sig_std[group == group2], sep = sep_by)
      } else {
        NA_character_
      },
      # Add confidence intervals for nodes
      confint_combined = if (all(c(group1, group2) %in% group)) {
        paste(confint[group == group1],
              confint[group == group2], sep = sep_by)
      } else {
        NA_character_
      },
      confint_std_combined = if (all(c(group1, group2) %in% group)) {
        paste(confint_std[group == group1],
              confint_std[group == group2], sep = sep_by)
      } else {
        NA_character_
      },
      across(c(x, y, shape, node_xmin, node_xmax, node_ymin, node_ymax,
               lhs, block, show, label_results), first),
      .groups = 'drop'
    ) |>
    filter(!is.na(est_combined)) |>
    # Set label based on standardized/unstandardized parameters with p_val and conf_int
    mutate(
      label = case_when(
        # Case 1: Both standardized and unstandardized are FALSE
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint_combined
          } else {
            ""
          },

        # Case 2: Both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")")
            }
          } else {
            if (conf_int) {
              paste0(est_combined, " (", est_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_combined, " (", est_std_combined, ")")
            }
          },

        # Case 3: Only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_std_combined, "\n", confint_std_combined)
            } else {
              est_sig_std_combined
            }
          } else {
            if (conf_int) {
              paste0(est_std_combined, "\n", confint_std_combined)
            } else {
              est_std_combined
            }
          },

        # Case 4: Only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, "\n", confint_combined)
            } else {
              est_sig_combined
            }
          } else {
            if (conf_int) {
              paste0(est_combined, "\n", confint_combined)
            } else {
              est_combined
            }
          },

        TRUE ~ ""  # Fallback (shouldn't be reached)
      )
    ) |>
    left_join(original_nodes_order, by = c("name", "op", "rhs")) |>
    arrange(original_order) |>
    select(-original_order)

  # Update the object
  tidysem_obj$edges <- edges_combined
  tidysem_obj$nodes <- nodes_combined

  return(tidysem_obj)
}

#' Combine tidySEM Groups for Bayesian Multi-Group Analysis
#'
#' This internal function combines parameter estimates from multiple groups within
#' a single Bayesian SEM tidySEM object. It merges estimates, HPD intervals, and
#' significance indicators for specified groups into a unified visualization object.
#'
#' @param tidysem_obj A tidySEM graph object containing multiple groups.
#' @param blavaan_fit A fitted Bayesian SEM object from blavaan containing the multi-group model.
#' @param group1 Character string specifying the name of the first group to compare.
#' @param group2 Character string specifying the name of the second group to compare.
#' @param sep_by Character string used to separate group values in the output
#'   (default: " | ").
#' @param standardized Logical indicating whether to include standardized estimates
#'   in the output (default: FALSE).
#' @param unstandardized Logical indicating whether to include unstandardized estimates
#'   in the output (default: TRUE).
#' @param p_val Logical indicating whether to include significance stars based on
#'   HPD intervals (default: FALSE).
#' @param conf_int Logical indicating whether to include HPD intervals
#'   in the output (default: FALSE).
#' @param ci_level Numeric value specifying the confidence level for HPD intervals
#'   (default: 0.95).
#'
#' @return A modified tidySEM object with combined group information
#'
#'
#'
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming tidysem_obj contains multiple groups and blavaan_fit is a multi-group model
#' combined <- combine_tidysem_groups_bayes(
#'   tidysem_obj, blavaan_fit,
#'   group1 = "Group1", group2 = "Group2",
#'   standardized = TRUE, unstandardized = TRUE,
#'   p_val = TRUE, conf_int = TRUE
#' )
#' }
#'
#'
#' @importFrom blavaan blavInspect
#' @importFrom dplyr distinct mutate left_join select group_by filter summarise across arrange case_when n first any_of row_number
#' @importFrom stats na.omit
#' @importFrom rlang .data
combine_tidysem_groups_bayes <- function(tidysem_obj, blavaan_fit, group1 = "", group2 = "",
                                         sep_by = " | ", standardized = FALSE, unstandardized = TRUE,
                                         p_val = FALSE, conf_int = FALSE, ci_level = 0.95) {

  # Get HPD intervals from blavaan for credible intervals
  hpd_intervals <- as.data.frame(blavaan::blavInspect(blavaan_fit, "hpd", level = ci_level))
  hpd_intervals$parameter <- rownames(hpd_intervals)

  add_significance_stars <- function(est_values, lower_values, upper_values) {
    significant <- !is.na(lower_values) & !is.na(upper_values) &
      (lower_values > 0 | upper_values < 0)
    ifelse(significant, paste0(est_values, "*"), est_values)
  }

  original_edges_order <- tidysem_obj$edges |>
    distinct(lhs, op, rhs) |>
    mutate(original_order = row_number())

  tidysem_obj$edges <- tidysem_obj$edges |>
    mutate(
      # Create parameter names for matching with HPD intervals
      param_name = case_when(
        op == "=~" ~ paste0(lhs, op, rhs),
        op == "~~" & lhs != rhs ~ paste0(lhs, op, rhs),
        op == "~~" & lhs == rhs ~ paste0("Variances.", lhs),
        op == "~1" ~ paste0("Means.", lhs),
        TRUE ~ paste0(lhs, op, rhs)
      ),
      group_num = match(group, unique(group)),
      param_name_full = ifelse(group_num == 1,
                               param_name,
                               paste0(param_name, ".g", group_num))
    ) |>
    left_join(hpd_intervals |> select(parameter, lower, upper),
              by = c("param_name_full" = "parameter")) |>
    mutate(
      confint = ifelse(!is.na(lower),
                       paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                       NA_character_),
      confint_std = confint,
      est_sig = if (p_val) add_significance_stars(est, lower, upper) else est,
      est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else est_std
    ) |>
    select(-param_name, -param_name_full, -group_num, -lower, -upper)

  tidysem_obj$edges <- tidysem_obj$edges |>
    mutate(across(c(est, est_std), ~ as.numeric(as.character(.x))))

  if (!is.null(tidysem_obj$nodes)) {
    tidysem_obj$nodes <- tidysem_obj$nodes |>
      mutate(across(c(est, est_std), ~ as.numeric(as.character(.x))))
  }


  edges_combined <- tidysem_obj$edges |>
    group_by(lhs, op, rhs) |>
    summarise(
      est_combined = paste(
        ifelse(is.na(est[group == group1]), "NA", round(est[group == group1], 2)),
        ifelse(is.na(est[group == group2]), "NA", round(est[group == group2], 2)),
        sep = sep_by
      ),
      est_sig_combined = paste(
        ifelse(is.na(est_sig[group == group1]), "NA", est_sig[group == group1]),
        ifelse(is.na(est_sig[group == group2]), "NA", est_sig[group == group2]),
        sep = sep_by
      ),
      # Add standardized estimates
      est_std_combined = if ("est_std" %in% names(tidysem_obj$edges)) {
        paste(
          ifelse(is.na(est_std[group == group1]), "NA", round(est_std[group == group1], 2)),
          ifelse(is.na(est_std[group == group2]), "NA", round(est_std[group == group2], 2)),
          sep = sep_by
        )
      },
      est_sig_std_combined = if ("est_sig_std" %in% names(tidysem_obj$edges)) {
        paste(
          ifelse(is.na(est_sig_std[group == group1]), "NA", est_sig_std[group == group1]),
          ifelse(is.na(est_sig_std[group == group2]), "NA", est_sig_std[group == group2]),
          sep = sep_by
        )
      } else if ("est_std" %in% names(tidysem_obj$edges)) {
        paste(
          ifelse(is.na(est_std[group == group1]), "NA", round(est_std[group == group1], 2)),
          ifelse(is.na(est_std[group == group2]), "NA", round(est_std[group == group2], 2)),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      confint_combined = paste(
        ifelse(is.na(confint[group == group1]), "NA", confint[group == group1]),
        ifelse(is.na(confint[group == group2]), "NA", confint[group == group2]),
        sep = sep_by
      ),
      confint_std_combined = paste(
        ifelse(is.na(confint_std[group == group1]), "NA", confint_std[group == group1]),
        ifelse(is.na(confint_std[group == group2]), "NA", confint_std[group == group2]),
        sep = sep_by
      ),
      across(c(from, to, arrow, connect_from, connect_to, curvature,
               linetype, block, show, label_results), first),
      .groups = 'drop'
    ) |>
    filter(!is.na(est_combined)) |>
    # CLEANUP: Replace "NA | NA" and NA with empty strings in confint columns
    mutate(
      across(c(est_combined, est_sig_combined, est_std_combined, est_sig_std_combined,
               confint_combined, confint_std_combined),
             ~ ifelse(.x == paste0("NA", sep_by, "NA"), "", .x))
    ) |>
    mutate(
      label = case_when(
        # Both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined),
                     paste0(est_sig_combined, " (", est_sig_std_combined, ")"))
            } else {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")")
            }
          } else {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_combined, " (", est_std_combined, ")\n", confint_combined),
                     paste0(est_combined, " (", est_std_combined, ")"))
            } else {
              paste0(est_combined, " (", est_std_combined, ")")
            }
          },

        # Only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_std_combined != "",
                     paste0(est_sig_std_combined, "\n", confint_std_combined),
                     est_sig_std_combined)
            } else {
              est_sig_std_combined
            }
          } else {
            if (conf_int) {
              ifelse(confint_std_combined != "",
                     paste0(est_std_combined, "\n", confint_std_combined),
                     est_std_combined)
            } else {
              est_std_combined
            }
          },

        # Only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_sig_combined, "\n", confint_combined),
                     est_sig_combined)
            } else {
              est_sig_combined
            }
          } else {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_combined, "\n", confint_combined),
                     est_combined)
            } else {
              est_combined
            }
          },

        # Neither standardized nor unstandardized
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint_combined
          } else {
            ""
          },

        TRUE ~ ""  # Fallback
      )
    ) |>
    left_join(original_edges_order, by = c("lhs", "op", "rhs")) |>
    arrange(original_order) |>
    select(-original_order)

  original_nodes_order <- tidysem_obj$nodes |>
    distinct(name, op, rhs) |>
    mutate(original_order = row_number())

  # Combine nodes (if nodes exist)
  if (!is.null(tidysem_obj$nodes)) {
    tidysem_obj$nodes <- tidysem_obj$nodes |>
      mutate(
        param_name = ifelse(op == "~1", paste0("Means.", name), NA_character_),
        group_num = match(group, unique(group)),
        param_name_full = ifelse(group_num == 1,
                                 param_name,
                                 paste0(param_name, ".g", group_num))
      ) |>
      left_join(hpd_intervals |> select(parameter, lower, upper),
                by = c("param_name_full" = "parameter")) |>
      mutate(
        confint = ifelse(!is.na(lower),
                         paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                         NA_character_),
        confint_std = confint,
        est_sig = if (p_val) add_significance_stars(est, lower, upper) else est,
        est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else est_std
      ) |>
      select(-param_name, -param_name_full, -group_num, -lower, -upper)

    nodes_combined <- tidysem_obj$nodes |>
      group_by(name, op, rhs) |>
      summarise(
        est_combined = if (all(c(group1, group2) %in% group)) {
          paste(round(est[group == group1], 2),
                round(est[group == group2], 2), sep = sep_by)
        } else {
          NA_character_
        },
        est_sig_combined = if (all(c(group1, group2) %in% group)) {
          paste(est_sig[group == group1],
                est_sig[group == group2], sep = sep_by)
        } else {
          NA_character_
        },
        est_std_combined = if (all(c(group1, group2) %in% group)) {
          paste(round(est_std[group == group1], 2),
                round(est_std[group == group2], 2), sep = sep_by)
        } else {
          NA_character_
        },
        est_sig_std_combined = if (all(c(group1, group2) %in% group)) {
          paste(est_sig_std[group == group1],
                est_sig_std[group == group2], sep = sep_by)
        } else {
          NA_character_
        },
        confint_combined = if (all(c(group1, group2) %in% group)) {
          paste(confint[group == group1],
                confint[group == group2], sep = sep_by)
        } else {
          NA_character_
        },
        confint_std_combined = if (all(c(group1, group2) %in% group)) {
          paste(confint_std[group == group1],
                confint_std[group == group2], sep = sep_by)
        } else {
          NA_character_
        },
        across(c(x, y, shape, node_xmin, node_xmax, node_ymin, node_ymax,
                 lhs, block, show, label_results), first),
        .groups = 'drop'
      ) |>
      filter(!is.na(est_combined)) |>
      # CLEANUP: Replace "NA | NA" and NA with empty strings in confint columns for nodes too
      mutate(
        confint_combined = ifelse(is.na(confint_combined) | confint_combined == paste0("NA ", sep_by, " NA"), "", confint_combined),
        confint_std_combined = ifelse(is.na(confint_std_combined) | confint_std_combined == paste0("NA ", sep_by, " NA"), "", confint_std_combined)
      ) |>
      mutate(
        label = case_when(
          standardized == TRUE & unstandardized == TRUE ~
            if (p_val) {
              if (conf_int) {
                ifelse(confint_combined != "",
                       paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined),
                       paste0(est_sig_combined, " (", est_sig_std_combined, ")"))
              } else {
                paste0(est_sig_combined, " (", est_sig_std_combined, ")")
              }
            } else {
              if (conf_int) {
                ifelse(confint_combined != "",
                       paste0(est_combined, " (", est_std_combined, ")\n", confint_combined),
                       paste0(est_combined, " (", est_std_combined, ")"))
              } else {
                paste0(est_combined, " (", est_std_combined, ")")
              }
            },

          # Only standardized
          standardized == TRUE & unstandardized == FALSE ~
            if (p_val) {
              if (conf_int) {
                ifelse(confint_std_combined != "",
                       paste0(est_sig_std_combined, "\n", confint_std_combined),
                       est_sig_std_combined)
              } else {
                est_sig_std_combined
              }
            } else {
              if (conf_int) {
                ifelse(confint_std_combined != "",
                       paste0(est_std_combined, "\n", confint_std_combined),
                       est_std_combined)
              } else {
                est_std_combined
              }
            },

          # Only unstandardized
          standardized == FALSE & unstandardized == TRUE ~
            if (p_val) {
              if (conf_int) {
                ifelse(confint_combined != "",
                       paste0(est_sig_combined, "\n", confint_combined),
                       est_sig_combined)
              } else {
                est_sig_combined
              }
            } else {
              if (conf_int) {
                ifelse(confint_combined != "",
                       paste0(est_combined, "\n", confint_combined),
                       est_combined)
              } else {
                est_combined
              }
            },

          # Neither standardized nor unstandardized
          standardized == FALSE & unstandardized == FALSE ~
            if (conf_int) {
              # Show unstandardized confidence interval without "\n"
              confint_combined
            } else {
              ""
            },

          TRUE ~ ""  # Fallback
        )
      )  |>
      left_join(original_nodes_order, by = c("name", "op", "rhs")) |>
      arrange(original_order) |>
      select(-original_order)

    tidysem_obj$nodes <- nodes_combined
  }

  # Update the object
  tidysem_obj$edges <- edges_combined

  return(tidysem_obj)
}

#' Combine tidySEM Objects for Group Comparison
#'
#' This internal function combines two tidySEM graph objects into a single object
#' for group comparison. It merges parameter estimates, confidence intervals,
#' and significance indicators from both groups for visualization.
#'
#' @param tidysem_obj1 A tidySEM graph object for the first group.
#' @param tidysem_obj2 A tidySEM graph object for the second group.
#' @param group1 Character string specifying the name for the first group
#'   (default: "Group1").
#' @param group2 Character string specifying the name for the second group
#'   (default: "Group2").
#' @param sep_by Character string used to separate group values in the output
#'   (default: " | ").
#' @param standardized Logical indicating whether to include standardized estimates
#'   in the output (default: FALSE).
#' @param unstandardized Logical indicating whether to include unstandardized estimates
#'   in the output (default: TRUE).
#' @param p_val Logical indicating whether to include significance indicators
#'   in the output (default: TRUE).
#' @param conf_int Logical indicating whether to include confidence intervals
#'   in the output (default: FALSE).
#'
#' @return A combined tidySEM object
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming tidysem_obj1 and tidysem_obj2 exist
#' combined <- combine_tidysem_objects(
#'   tidysem_obj1, tidysem_obj2,
#'   group1 = "Control", group2 = "Treatment",
#'   standardized = TRUE, unstandardized = TRUE,
#'   p_val = TRUE, conf_int = TRUE
#' )
#' }
#'
#'
#' @importFrom dplyr distinct mutate left_join select group_by filter summarise across arrange bind_rows n first any_of row_number
#' @importFrom stats na.omit
#' @importFrom rlang .data
#' @importFrom methods is
combine_tidysem_objects <- function(tidysem_obj1, tidysem_obj2, group1 = "Group1", group2 = "Group2",
                                    sep_by = " | ", standardized = FALSE, unstandardized = TRUE,
                                    p_val = TRUE, conf_int = FALSE) {

  if (!all(c("edges", "nodes") %in% names(tidysem_obj1)) ||
      !all(c("edges", "nodes") %in% names(tidysem_obj2))) {
    stop("Both objects must be tidySEM graph objects with 'edges' and 'nodes' components")
  }

  tidysem_obj1$edges$group <- group1
  tidysem_obj1$nodes$group <- group1

  tidysem_obj2$edges$group <- group2
  tidysem_obj2$nodes$group <- group2

  original_edges_order <- tidysem_obj1$edges |>
    distinct(lhs, op, rhs) |>
    mutate(original_order = row_number())

  edges_combined <- bind_rows(tidysem_obj1$edges, tidysem_obj2$edges)

  nodes_combined <- tidysem_obj1$nodes

  edges_combined <- edges_combined |>
    mutate(across(any_of(c("est", "est_std")), ~ as.numeric(as.character(.x))))

  nodes_combined <- nodes_combined |>
    mutate(across(any_of(c("est", "est_std")), ~ as.numeric(as.character(.x))))

  edges_final <- edges_combined |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>  # Only keep parameters that exist in both groups
    summarise(
      est_combined = paste(
        round(est[group == group1], 2),
        round(est[group == group2], 2),
        sep = sep_by
      ),
      est_sig_combined = if ("est_sig" %in% names(edges_combined)) {
        paste(
          est_sig[group == group1],
          est_sig[group == group2],
          sep = sep_by
        )
      } else {
        paste(
          round(est[group == group1], 2),
          round(est[group == group2], 2),
          sep = sep_by
        )
      },
      est_std_combined = if ("est_std" %in% names(edges_combined)) {
        paste(
          round(est_std[group == group1], 2),
          round(est_std[group == group2], 2),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      est_sig_std_combined = if (all(c("est_sig_std") %in% names(edges_combined))) {
        paste(
          est_sig_std[group == group1],
          est_sig_std[group == group2],
          sep = sep_by
        )
      } else if ("est_std" %in% names(edges_combined)) {
        paste(
          round(est_std[group == group1], 2),
          round(est_std[group == group2], 2),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      confint_combined = if ("confint" %in% names(edges_combined)) {
        paste(
          confint[group == group1],
          confint[group == group2],
          sep = sep_by
        )
      } else {
        NA_character_
      },
      confint_std_combined = if ("confint_std" %in% names(edges_combined)) {
        paste(
          confint_std[group == group1],
          confint_std[group == group2],
          sep = sep_by
        )
      } else {
        NA_character_
      },
      across(any_of(c("from", "to", "arrow", "connect_from", "connect_to",
                      "curvature", "linetype", "block", "show", "label_results")),
             ~ first(na.omit(.x))),
      .groups = 'drop'
    ) |>
    mutate(
      label = case_when(
        # Case 1: Both standardized and unstandardized are FALSE
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint_combined
          } else {
            ""
          },

        # Case 2: Both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")")
            }
          } else {
            if (conf_int) {
              paste0(est_combined, " (", est_std_combined, ")\n", confint_combined)
            } else {
              paste0(est_combined, " (", est_std_combined, ")")
            }
          },

        # Case 3: Only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_std_combined, "\n", confint_std_combined)
            } else {
              est_sig_std_combined
            }
          } else {
            if (conf_int) {
              paste0(est_std_combined, "\n", confint_std_combined)
            } else {
              est_std_combined
            }
          },

        # Case 4: Only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              paste0(est_sig_combined, "\n", confint_combined)
            } else {
              est_sig_combined
            }
          } else {
            if (conf_int) {
              paste0(est_combined, "\n", confint_combined)
            } else {
              est_combined
            }
          },

        # Fallback (shouldn't be reached)
        TRUE ~ ""
      )
    ) |>
    left_join(original_edges_order, by = c("lhs", "op", "rhs")) |>
    arrange(original_order) |>
    select(-original_order)

  nodes_combined <- nodes_combined |> select(-group)

  combined_tidysem <- list(
    edges = edges_final,
    nodes = nodes_combined
  )

  if (!is.null(attr(tidysem_obj1, "class"))) {
    class(combined_tidysem) <- class(tidysem_obj1)
  }

  return(combined_tidysem)
}

#' Combine tidySEM Objects for Bayesian Group Comparison
#'
#' This internal function combines two tidySEM graph objects from Bayesian SEM models
#' into a single object for group comparison. It merges parameter estimates,
#' HPD intervals, and significance indicators from both groups.
#'
#' @param tidysem_obj1 A tidySEM graph object for the first group.
#' @param tidysem_obj2 A tidySEM graph object for the second group.
#' @param blavaan_fit1 A fitted Bayesian SEM object from blavaan for the first group.
#' @param blavaan_fit2 A fitted Bayesian SEM object from blavaan for the second group.
#' @param group1 Character string specifying the name for the first group
#'   (default: "Group1").
#' @param group2 Character string specifying the name for the second group
#'   (default: "Group2").
#' @param sep_by Character string used to separate group values in the output
#'   (default: " | ").
#' @param standardized Logical indicating whether to include standardized estimates
#'   in the output (default: FALSE).
#' @param unstandardized Logical indicating whether to include unstandardized estimates
#'   in the output (default: TRUE).
#' @param p_val Logical indicating whether to include significance stars based on
#'   HPD intervals (default: TRUE).
#' @param conf_int Logical indicating whether to include confidence intervals
#'   in the output (default: FALSE).
#' @param ci_level Numeric value specifying the confidence level for HPD intervals
#'   (default: 0.95).
#'
#' @return A combined tidySEM object with bayesian stats
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming tidysem_obj1, tidysem_obj2, blavaan_fit1, and blavaan_fit2 exist
#' combined <- combine_tidysem_objects_bayes(
#'   tidysem_obj1, tidysem_obj2, blavaan_fit1, blavaan_fit2,
#'   group1 = "Control", group2 = "Treatment",
#'   standardized = TRUE, unstandardized = TRUE,
#'   p_val = TRUE, conf_int = TRUE
#' )
#' }
#'
#' @importFrom blavaan blavInspect
#' @importFrom dplyr distinct mutate left_join select group_by filter summarise across arrange bind_rows n first any_of row_number
#' @importFrom stats na.omit
#' @importFrom rlang .data
#' @importFrom methods is
combine_tidysem_objects_bayes <- function(tidysem_obj1, tidysem_obj2, blavaan_fit1, blavaan_fit2,
                                          group1 = "Group1", group2 = "Group2",
                                          sep_by = " | ", standardized = FALSE, unstandardized = TRUE,
                                          p_val = TRUE, conf_int = FALSE, ci_level = 0.95) {

  if (!all(c("edges", "nodes") %in% names(tidysem_obj1)) ||
      !all(c("edges", "nodes") %in% names(tidysem_obj2))) {
    stop("Both objects must be tidySEM graph objects with 'edges' and 'nodes' components")
  }

  hpd_intervals1 <- as.data.frame(blavaan::blavInspect(blavaan_fit1, "hpd", level = ci_level))
  hpd_intervals1$parameter <- rownames(hpd_intervals1)

  hpd_intervals2 <- as.data.frame(blavaan::blavInspect(blavaan_fit2, "hpd", level = ci_level))
  hpd_intervals2$parameter <- rownames(hpd_intervals2)

  add_significance_stars <- function(est_values, lower_values, upper_values) {
    significant <- !is.na(lower_values) & !is.na(upper_values) &
      (lower_values > 0 | upper_values < 0)
    ifelse(significant, paste0(est_values, "*"), est_values)
  }

  original_edges_order <- tidysem_obj1$edges |>
    distinct(lhs, op, rhs) |>
    mutate(original_order = row_number())

  tidysem_obj1$edges <- tidysem_obj1$edges |>
    mutate(
      param_name = case_when(
        op == "=~" ~ paste0(lhs, op, rhs),
        op == "~~" & lhs != rhs ~ paste0(lhs, op, rhs),
        op == "~~" & lhs == rhs ~ paste0("Variances.", lhs),
        op == "~1" ~ paste0("Means.", lhs),
        TRUE ~ paste0(lhs, op, rhs)
      )
    ) |>
    left_join(hpd_intervals1 |> select(parameter, lower, upper),
              by = c("param_name" = "parameter")) |>
    mutate(
      confint = ifelse(!is.na(lower),
                       paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                       NA_character_),
      confint_std = confint,
      est_sig = if (p_val) add_significance_stars(est, lower, upper) else as.character(est),
      est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else as.character(est_std),
      group = group1
    ) |>
    select(-param_name, -lower, -upper)

  tidysem_obj2$edges <- tidysem_obj2$edges |>
    mutate(
      param_name = case_when(
        op == "=~" ~ paste0(lhs, op, rhs),
        op == "~~" & lhs != rhs ~ paste0(lhs, op, rhs),
        op == "~~" & lhs == rhs ~ paste0("Variances.", lhs),
        op == "~1" ~ paste0("Means.", lhs),
        TRUE ~ paste0(lhs, op, rhs)
      )
    ) |>
    left_join(hpd_intervals2 |> select(parameter, lower, upper),
              by = c("param_name" = "parameter")) |>
    mutate(
      confint = ifelse(!is.na(lower),
                       paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                       NA_character_),
      confint_std = confint,
      est_sig = if (p_val) add_significance_stars(est, lower, upper) else as.character(est),
      est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else as.character(est_std),
      group = group2
    ) |>
    select(-param_name, -lower, -upper)

  edges_combined <- bind_rows(tidysem_obj1$edges, tidysem_obj2$edges)

  edges_combined <- edges_combined |>
    mutate(across(any_of(c("est", "est_std")), ~ as.numeric(as.character(.x))))
  edges_final <- edges_combined |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>  # Only keep parameters that exist in both groups
    summarise(
      est_combined = paste(
        ifelse(is.na(est[group == group1]), "NA", round(est[group == group1], 2)),
        ifelse(is.na(est[group == group2]), "NA", round(est[group == group2], 2)),
        sep = sep_by
      ),
      est_sig_combined = paste(
        ifelse(is.na(est_sig[group == group1]), "NA", est_sig[group == group1]),
        ifelse(is.na(est_sig[group == group2]), "NA", est_sig[group == group2]),
        sep = sep_by
      ),
      est_std_combined = if ("est_std" %in% names(edges_combined)) {
        paste(
          ifelse(is.na(est_std[group == group1]), "NA", round(est_std[group == group1], 2)),
          ifelse(is.na(est_std[group == group2]), "NA", round(est_std[group == group2], 2)),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      est_sig_std_combined = if ("est_sig_std" %in% names(edges_combined)) {
        paste(
          ifelse(is.na(est_sig_std[group == group1]), "NA", est_sig_std[group == group1]),
          ifelse(is.na(est_sig_std[group == group2]), "NA", est_sig_std[group == group2]),
          sep = sep_by
        )
      } else if ("est_std" %in% names(edges_combined)) {
        paste(
          ifelse(is.na(est_std[group == group1]), "NA", round(est_std[group == group1], 2)),
          ifelse(is.na(est_std[group == group2]), "NA", round(est_std[group == group2], 2)),
          sep = sep_by
        )
      } else {
        NA_character_
      },
      confint_combined = paste(
        ifelse(is.na(confint[group == group1]), "NA", confint[group == group1]),
        ifelse(is.na(confint[group == group2]), "NA", confint[group == group2]),
        sep = sep_by
      ),
      confint_std_combined = paste(
        ifelse(is.na(confint_std[group == group1]), "NA", confint_std[group == group1]),
        ifelse(is.na(confint_std[group == group2]), "NA", confint_std[group == group2]),
        sep = sep_by
      ),
      across(any_of(c("from", "to", "arrow", "connect_from", "connect_to",
                      "curvature", "linetype", "block", "show", "label_results")),
             ~ first(na.omit(.x))),
      .groups = 'drop'
    ) |>
    # Clean up NA values in confidence intervals
    mutate(
      across(c(est_combined, est_sig_combined, est_std_combined, est_sig_std_combined,
               confint_combined, confint_std_combined),
             ~ ifelse(.x == paste0("NA", sep_by, "NA"), "", .x))
    ) |>
    mutate(
      label = case_when(
        # Case 1: Both standardized and unstandardized are FALSE
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint_combined
          } else {
            ""
          },

        # Case 2: Both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_sig_combined, " (", est_sig_std_combined, ")\n", confint_combined),
                     paste0(est_sig_combined, " (", est_sig_std_combined, ")"))
            } else {
              paste0(est_sig_combined, " (", est_sig_std_combined, ")")
            }
          } else {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_combined, " (", est_std_combined, ")\n", confint_combined),
                     paste0(est_combined, " (", est_std_combined, ")"))
            } else {
              paste0(est_combined, " (", est_std_combined, ")")
            }
          },

        # Case 3: Only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_std_combined != "",
                     paste0(est_sig_std_combined, "\n", confint_std_combined),
                     est_sig_std_combined)
            } else {
              est_sig_std_combined
            }
          } else {
            if (conf_int) {
              ifelse(confint_std_combined != "",
                     paste0(est_std_combined, "\n", confint_std_combined),
                     est_std_combined)
            } else {
              est_std_combined
            }
          },

        # Case 4: Only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val) {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_sig_combined, "\n", confint_combined),
                     est_sig_combined)
            } else {
              est_sig_combined
            }
          } else {
            if (conf_int) {
              ifelse(confint_combined != "",
                     paste0(est_combined, "\n", confint_combined),
                     est_combined)
            } else {
              est_combined
            }
          },

        # Fallback (shouldn't be reached)
        TRUE ~ ""
      )
    ) |>
    left_join(original_edges_order, by = c("lhs", "op", "rhs")) |>
    arrange(original_order) |>
    select(-original_order)

  nodes_final <- tidysem_obj1$nodes

  # Create the combined tidySEM object
  combined_tidysem <- list(
    edges = edges_final,
    nodes = nodes_final
  )

  if (!is.null(attr(tidysem_obj1, "class"))) {
    class(combined_tidysem) <- class(tidysem_obj1)
  }

  return(combined_tidysem)
}

#' Combine Bayesian SEM Model Parameters for Group Comparison
#'
#' This internal function combines parameter estimates, standardized solutions,
#' and HPD intervals from two Bayesian structural equation models for group comparison.
#' It creates a comparison table showing parameter estimates and credible intervals
#' for both groups side by side.
#'
#' @param fit1 A fitted Bayesian SEM object from blavaan for the first group.
#' @param fit2 A fitted Bayesian SEM object from blavaan for the second group.
#' @param group1 Character string specifying the name for the first group
#'   (default: "Group1").
#' @param group2 Character string specifying the name for the second group
#'   (default: "Group2").
#' @param sep_by Character string used to separate group values in the output
#'   (default: "|").
#'
#' @return A data frame with the following columns:
#' \itemize{
#'   \item \code{lhs}: Left-hand side variable in the SEM parameter
#'   \item \code{op}: Operator type ("=~" for loadings, "~~" for covariances, "~1" for intercepts)
#'   \item \code{rhs}: Right-hand side variable in the SEM parameter
#'   \item \code{est}: Unstandardized parameter estimates formatted as "group1|group2"
#'   \item \code{std}: Standardized parameter estimates formatted as "group1|group2"
#'   \item \code{confint_unstd}: HPD credible intervals formatted as "[lower1,upper1]|[lower2,upper2]"
#' }
#'
#'
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming fit1 and fit2 are fitted Bayesian SEM models
#' comparison <- combine_model_parameters_bayes(fit1, fit2,
#'                                              group1 = "Control",
#'                                              group2 = "Treatment")
#' print(comparison)
#' }
#' @importFrom lavaan parameterEstimates standardizedSolution
#' @importFrom blavaan blavInspect
#' @importFrom dplyr group_by filter summarise mutate rowwise ungroup select inner_join n case_when
#' @importFrom rlang .data
combine_model_parameters_bayes <- function(fit1, fit2, group1 = "Group1", group2 = "Group2", sep_by = "|") {
  # Get parameter estimates for both models
  params1 <- lavaan::parameterEstimates(fit1)
  params2 <- lavaan::parameterEstimates(fit2)

  # Get standardized solutions for both models
  std_sol1 <- lavaan::standardizedSolution(fit1)
  std_sol2 <- lavaan::standardizedSolution(fit2)

  # Get HPD intervals for both models
  hpd1 <- as.data.frame(blavaan::blavInspect(fit1, "hpd"))
  hpd2 <- as.data.frame(blavaan::blavInspect(fit2, "hpd"))

  params1$group_name <- group1
  params2$group_name <- group2
  std_sol1$group_name <- group1
  std_sol2$group_name <- group2

  combined_params <- rbind(params1, params2)
  combined_std <- rbind(std_sol1, std_sol2)

  combined_params <- combined_params[combined_params$op %in% c("=~", "~~", "~1", "~"), ]
  combined_std <- combined_std[combined_std$op %in% c("=~", "~~", "~1", "~"), ]

  # combined_params <- combined_params[!(combined_params$op == "~~" & combined_params$lhs == combined_params$rhs), ]
  # combined_std <- combined_std[!(combined_std$op == "~~" & combined_std$lhs == combined_std$rhs), ]

  create_param_name <- function(lhs, op, rhs) {
    dplyr::case_when(
      op == "=~" ~ paste0(lhs, "=~", rhs),
      op == "~~" & lhs != rhs ~ paste0(lhs, "~~", rhs),
      op == "~~" & lhs == rhs ~ paste0(lhs, "~~", rhs),
      op == "~1" ~ paste0(lhs, "~1"),
      TRUE ~ paste0(lhs, op, rhs)
    )
  }

  comparison_unstd <- combined_params |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      est = paste0(
        round(est[group_name == group1], 2), sep_by,
        round(est[group_name == group2], 2)
      ),
      group1_est = est[group_name == group1],
      group2_est = est[group_name == group2],
      .groups = 'drop'
    )

  comparison_unstd <- comparison_unstd |>
    mutate(
      param_name_group1 = create_param_name(lhs, op, rhs),
      param_name_group2 = create_param_name(lhs, op, rhs)
    ) |>
    rowwise() |>
    mutate(
      group1_hpd_lower = ifelse(param_name_group1 %in% rownames(hpd1),
                                hpd1[param_name_group1, "lower"], NA_real_),
      group1_hpd_upper = ifelse(param_name_group1 %in% rownames(hpd1),
                                hpd1[param_name_group1, "upper"], NA_real_),
      group2_hpd_lower = ifelse(param_name_group2 %in% rownames(hpd2),
                                hpd2[param_name_group2, "lower"], NA_real_),
      group2_hpd_upper = ifelse(param_name_group2 %in% rownames(hpd2),
                                hpd2[param_name_group2, "upper"], NA_real_)
    ) |>
    ungroup() |>
    mutate(
      confint_unstd = paste0(
        "[", round(group1_hpd_lower, 2), ",", round(group1_hpd_upper, 2), "]",
        sep_by,
        "[", round(group2_hpd_lower, 2), ",", round(group2_hpd_upper, 2), "]"
      )
    )

  comparison_std <- combined_std |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      std = paste0(
        round(est.std[group_name == group1], 2), sep_by,
        round(est.std[group_name == group2], 2)
      ),
      group1_std = est.std[group_name == group1],
      group2_std = est.std[group_name == group2],
      .groups = 'drop'
    )

  comparison_table <- comparison_unstd |>
    inner_join(comparison_std, by = c("lhs", "op", "rhs")) |>
    select(lhs, op, rhs, est, std, confint_unstd)

  return(comparison_table)
}

#' Combine SEM Model Parameters for Group Comparison
#'
#' This internal function combines parameter estimates, standardized solutions,
#' and confidence intervals from two structural equation models for group comparison.
#' It creates a comprehensive comparison table showing both unstandardized and
#' standardized parameter estimates with confidence intervals for both groups.
#'
#' @param fit1 A fitted SEM object from lavaan for the first group.
#' @param fit2 A fitted SEM object from lavaan for the second group.
#' @param group1 Character string specifying the name for the first group
#'   (default: "Group1").
#' @param group2 Character string specifying the name for the second group
#'   (default: "Group2").
#' @param sep_by Character string used to separate group values in the output
#'   (default: "|").
#' @param ci_level Numeric value specifying the confidence level for intervals
#'   (default: 0.95, for 95% confidence intervals).
#'
#' @return A data frame with the following columns:
#' \itemize{
#'   \item \code{lhs}: Left-hand side variable in the SEM parameter
#'   \item \code{op}: Operator type ("=~" for loadings, "~~" for covariances, "~1" for intercepts)
#'   \item \code{rhs}: Right-hand side variable in the SEM parameter
#'   \item \code{est}: Unstandardized parameter estimates formatted as "group1|group2"
#'   \item \code{std}: Standardized parameter estimates formatted as "group1|group2"
#'   \item \code{confint_unstd}: Unstandardized confidence intervals formatted as "[lower1,upper1]|[lower2,upper2]"
#'   \item \code{confint_std}: Standardized confidence intervals formatted as "[lower1,upper1]|[lower2,upper2]"
#' }
#'
#'
#' @keywords internal
#' @noRd
#' @examples
#' \dontrun{
#' # Assuming fit1 and fit2 are fitted SEM models from lavaan
#' comparison <- combine_model_parameters(fit1, fit2,
#'                                       group1 = "Control",
#'                                       group2 = "Treatment",
#'                                       ci_level = 0.95)
#' }
#'
#'
#' @importFrom lavaan parameterEstimates standardizedSolution lavInspect
#' @importFrom dplyr  group_by filter summarise mutate select inner_join n bind_rows rename
#' @importFrom rlang .data
combine_model_parameters <- function(fit1, fit2, group1 = "Group1", group2 = "Group2", sep_by = "|", ci_level = 0.95) {
  extract_params <- function(fit, target_group) {
    group_info <- lavInspect(fit, "group.label")

    extract_and_label <- function(extract_func) {
      df <- extract_func(fit, ci = TRUE, level = ci_level)

      if (length(group_info) > 0) {
        group_mapping <- setNames(group_info, seq_along(group_info))
        df <- df |>
          mutate(group = ifelse(group %in% names(group_mapping),
                                group_mapping[as.character(group)], group)) |>
          filter(group == target_group)
      } else {
        df$group <- target_group
      }

      df
    }

    list(
      unstd = extract_and_label(parameterEstimates),
      std = extract_and_label(standardizedSolution)
    )
  }

  params1 <- extract_params(fit1, group1)
  params2 <- extract_params(fit2, group2)

  combine_and_filter <- function(sol1, sol2) {
    combined <- bind_rows(sol1, sol2)
    combined[combined$op %in% c("=~", "~~", "~1", "~"), ]
  }

  combined_unstd <- combine_and_filter(params1$unstd, params2$unstd)
  combined_std <- combine_and_filter(params1$std, params2$std)

  create_comparison <- function(combined_df, value_col, round_digits = 2) {
    combined_df |>
      group_by(lhs, op, rhs) |>
      filter(n() == 2) |>
      summarise(
        value = paste0(
          round(.data[[value_col]][group == group1], round_digits),
          sep_by,
          round(.data[[value_col]][group == group2], round_digits)
        ),
        confint = paste0(
          "[", round(ci.lower[group == group1], round_digits), ",",
          round(ci.upper[group == group1], round_digits), "]", sep_by,
          "[", round(ci.lower[group == group2], round_digits), ",",
          round(ci.upper[group == group2], round_digits), "]"
        ),
        .groups = 'drop'
      )
  }

  comparison_unstd <- create_comparison(combined_unstd, "est") |>
    rename(est = value, confint_unstd = confint)

  comparison_std <- create_comparison(combined_std, "est.std") |>
    rename(std = value, confint_std = confint)

  inner_join(comparison_unstd, comparison_std, by = c("lhs", "op", "rhs")) |>
    select(lhs, op, rhs, est, std, confint_unstd, confint_std)
}

# Get estimate differences (frequentist)
#' @importFrom lavaan lavInspect parameterEstimates
#' @importFrom dplyr mutate group_by filter n summarise ungroup bind_rows
#' @importFrom stats pnorm setNames
#' @keywords internal
#' @noRd
get_est_differences <- function(fit,
                                alpha = 0.05,
                                p_adjust = "none",
                                test_type = "pairwise",
                                group1 = "",
                                group2 = "") {

  group_info <- lavInspect(fit, "group.label")
  group_mapping <- setNames(group_info, 1:length(group_info))

  params <- parameterEstimates(fit) |>
    mutate(group = ifelse(group %in% names(group_mapping),
                          group_mapping[as.character(group)],
                          group))

  params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]

  pairwise_results <- data.frame()

  multi_group_params <- params |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() >= 2) |>
    ungroup()

  if (test_type == "pairwise") {
    if (group1 != "" && group2 != "") {
      all_groups <- unique(multi_group_params$group)
      if (!group1 %in% all_groups) {
        stop("Group '", group1, "' not found. Available groups: ", paste(all_groups, collapse = ", "))
      }
      if (!group2 %in% all_groups) {
        stop("Group '", group2, "' not found. Available groups: ", paste(all_groups, collapse = ", "))
      }

      group_levels <- c(group1, group2)
    } else {
      # Use all groups if none specified
      group_levels <- unique(multi_group_params$group)
    }

    for (i in 1:(length(group_levels)-1)) {
      for (j in (i+1):length(group_levels)) {
        current_group1 <- group_levels[i]
        current_group2 <- group_levels[j]

        pair_diffs <- multi_group_params |>
          group_by(lhs, op, rhs) |>
          filter(group %in% c(current_group1, current_group2)) |>
          filter(dplyr::n() == 2) |>
          summarise(
            comparison = paste(current_group1, "vs", current_group2),
            p_value = if ("se" %in% names(multi_group_params)) {
              2 * (1 - pnorm(abs(diff(est)) / sqrt(sum(se^2))))
            } else {
              NA_real_  # Return NA if se column doesn't exist
            },
            .groups = 'drop'
          )

        pairwise_results <- bind_rows(pairwise_results, pair_diffs)
      }
    }
  }

  pairwise_results$significant <- pairwise_results$p_value < alpha

  results <- pairwise_results

  return(results)
}

# Get estimate differences (Bayesian)
#' @importFrom lavaan parameterEstimates
#' @importFrom blavaan blavInspect
#' @importFrom dplyr mutate group_by filter n group_map bind_rows
#' @importFrom stats setNames
#' @keywords internal
#' @noRd
get_est_differences_bayes <- function(fit,
                                      p_adjust = "none",
                                      test_type = "pairwise",
                                      group1 = "",
                                      group2 = "",
                                      rope = c(-0.1, 0.1)) {

  group_info <- blavaan::blavInspect(fit, "group.label")
  group_mapping <- setNames(group_info, 1:length(group_info))

  # Get parameter estimates and HPD intervals
  params <- lavaan::parameterEstimates(fit)
  hpd_intervals <- as.data.frame(blavaan::blavInspect(fit, "hpd"))

  params$group <- ifelse(params$group %in% names(group_mapping),
                         group_mapping[as.character(params$group)],
                         params$group)

  params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]

  pairwise_results <- data.frame()


  multi_group_params <- params |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() >= 2) |>
    ungroup()

  if (test_type == "pairwise") {
    if (group1 != "" && group2 != "") {
      all_groups <- unique(multi_group_params$group)
      if (!group1 %in% all_groups) {
        stop("Group '", group1, "' not found. Available groups: ", paste(all_groups, collapse = ", "))
      }
      if (!group2 %in% all_groups) {
        stop("Group '", group2, "' not found. Available groups: ", paste(all_groups, collapse = ", "))
      }
      group_levels <- c(group1, group2)
    } else {
      group_levels <- unique(multi_group_params$group)
    }

    for (i in 1:(length(group_levels)-1)) {
      for (j in (i+1):length(group_levels)) {
        current_group1 <- group_levels[i]
        current_group2 <- group_levels[j]

        group1_num <- which(group_info == current_group1)
        group2_num <- which(group_info == current_group2)

        pair_diffs <- multi_group_params |>
          group_by(lhs, op, rhs) |>
          filter(group %in% c(current_group1, current_group2)) |>
          filter(dplyr::n() == 2) |>
          group_map(~ {
            diff_est <- .x$est[1] - .x$est[2]

            # Get parameter names for HPD intervals
            param_name_base <- paste0(.x$lhs[1], .x$op[1], .x$rhs[1])
            param_name1 <- if (group1_num == 1) param_name_base else paste0(param_name_base, ".g", group1_num)
            param_name2 <- if (group2_num == 1) param_name_base else paste0(param_name_base, ".g", group2_num)

            # Get HPD intervals
            hpd_idx1 <- which(rownames(hpd_intervals) == param_name1)
            hpd_idx2 <- which(rownames(hpd_intervals) == param_name2)

            if (length(hpd_idx1) > 0 && length(hpd_idx2) > 0) {
              hpd1_lower <- hpd_intervals$lower[hpd_idx1]
              hpd1_upper <- hpd_intervals$upper[hpd_idx1]
              hpd2_lower <- hpd_intervals$lower[hpd_idx2]
              hpd2_upper <- hpd_intervals$upper[hpd_idx2]

              # Conservative bounds for difference
              diff_lower <- hpd1_lower - hpd2_upper
              diff_upper <- hpd1_upper - hpd2_lower

              # Bayesian significance tests
              credible_excludes_zero <- diff_lower > 0 | diff_upper < 0
              excludes_rope <- diff_lower > rope[2] | diff_upper < rope[1]

              data.frame(
                lhs = .x$lhs[1],
                op = .x$op[1],
                rhs = .x$rhs[1],
                comparison = paste(current_group1, "vs", current_group2),
                posterior_mean_diff = diff_est,
                credible_interval = paste0("[", round(diff_lower, 2), ", ", round(diff_upper, 2), "]"),
                excludes_zero = credible_excludes_zero,
                excludes_rope = excludes_rope,
                rope_lower = rope[1],
                rope_upper = rope[2],
                stringsAsFactors = FALSE
              )
            } else {
              data.frame(
                lhs = .x$lhs[1],
                op = .x$op[1],
                rhs = .x$rhs[1],
                comparison = paste(current_group1, "vs", current_group2),
                posterior_mean_diff = diff_est,
                credible_interval = NA,
                excludes_zero = NA,
                excludes_rope = NA,
                rope_lower = rope[1],
                rope_upper = rope[2],
                stringsAsFactors = FALSE
              )
            }
          }, .keep = TRUE) |>
          bind_rows()

        pairwise_results <- bind_rows(pairwise_results, pair_diffs)
      }
    }
  }

  if (nrow(pairwise_results) > 0) {
    pairwise_results$significant <- pairwise_results$excludes_zero
  }

  results <- pairwise_results
  return(results)
}

#' Generate Bayesian model comparison table
#'
#' Creates a comparison table for Bayesian structural equation models, including
#' parameter estimates, credible intervals, and significance tests for group differences.
#' This function extracts and compares parameter estimates between two groups in a
#' Bayesian SEM framework, incorporating Highest Posterior Density (HPD) intervals
#' and Region of Practical Equivalence (ROPE) tests.
#'
#' @param fit A blavaan object containing the fitted Bayesian SEM model
#' @param rope Numeric vector of length 2 specifying the Region of Practical Equivalence
#'   bounds. Parameters with credible intervals entirely outside this range are
#'   considered practically significant. Default is c(-0.1, 0.1)
#' @param group1 Character string specifying the first group for comparison.
#'   Must match a group label in the fitted model
#' @param group2 Character string specifying the second group for comparison.
#'   Must match a group label in the fitted model
#' @param sep_by Character separator used in the comparison columns to distinguish
#'   between group1 and group2 values. Default is "|"
#'
#' @return A data frame containing the comparison table
#'
#' @importFrom blavaan blavInspect
#' @importFrom lavaan parameterEstimates standardizedSolution
#' @importFrom dplyr filter mutate distinct group_by summarise rowwise ungroup arrange select rename inner_join left_join case_when row_number
#' @importFrom rlang .data
#'
#' @keywords internal
#' @noRd
get_comparison_table_bayes <- function(fit, rope = c(-0.1, 0.1), group1 = "", group2 = "", sep_by = "|") {
  sig_diffs <- get_est_differences_bayes(fit = fit, rope = rope, group1 = group1, group2 = group2)

  if (nrow(sig_diffs) == 0) {
    return(data.frame())
  }

  # Get group information for blavaan
  group_info <- blavaan::blavInspect(fit, "group.label")
  group_mapping <- setNames(group_info, 1:length(group_info))

  # Get parameter estimates and HPD intervals
  params <- lavaan::parameterEstimates(fit)
  hpd_intervals <- as.data.frame(blavaan::blavInspect(fit, "hpd"))

  params$group <- ifelse(params$group %in% names(group_mapping),
                         group_mapping[as.character(params$group)],
                         params$group)

  params <- params[params$op %in% c("=~", "~1", "~~", "~"), ]

  # Get standardized solutions for blavaan
  std_solution <- lavaan::standardizedSolution(fit)
  std_solution <- std_solution |>
    filter(op %in% c("=~", "~1", "~~", "~")) |>
    mutate(group = ifelse(group %in% 1:length(group_info),
                          group_info[group],
                          group))

  group1_order <- params |>
    filter(group == group1) |>
    distinct(lhs, op, rhs) |>
    mutate(order = row_number())

  # Get unstandardized comparisons with HPD intervals
  group_comparisons <- params |>
    filter(group %in% c(group1, group2)) |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      group1_est = est[group == group1],
      group2_est = est[group == group2],
      # Get group numbers for HPD interval lookup
      group1_num = which(group_info == group1),
      group2_num = which(group_info == group2),
      .groups = 'drop'
    ) |>
    mutate(
      # Create parameter names for HPD lookup
      param_base = dplyr::case_when(
        op == "=~" ~ paste0(lhs, op, rhs),
        op == "~~" & lhs != rhs ~ paste0(lhs, op, rhs),
        op == "~~" & lhs == rhs ~ paste0("Variances.", lhs),
        op == "~1" ~ paste0("Means.", lhs),
        TRUE ~ paste0(lhs, op, rhs)
      ),
      # Create parameter names with group suffixes
      param_group1 = if (first(group1_num) == 1) param_base else paste0(param_base, ".g", first(group1_num)),
      param_group2 = if (first(group2_num) == 1) param_base else paste0(param_base, ".g", first(group2_num))
    ) |>
    rowwise() |>
    mutate(
      # Get HPD intervals for each group
      group1_hpd_lower = ifelse(param_group1 %in% rownames(hpd_intervals),
                                hpd_intervals[param_group1, "lower"], NA_real_),
      group1_hpd_upper = ifelse(param_group1 %in% rownames(hpd_intervals),
                                hpd_intervals[param_group1, "upper"], NA_real_),
      group2_hpd_lower = ifelse(param_group2 %in% rownames(hpd_intervals),
                                hpd_intervals[param_group2, "lower"], NA_real_),
      group2_hpd_upper = ifelse(param_group2 %in% rownames(hpd_intervals),
                                hpd_intervals[param_group2, "upper"], NA_real_)
    ) |>
    ungroup() |>
    mutate(
      # Create formatted credible intervals
      confint_unstd = paste0(
        "[", round(group1_hpd_lower, 2), ",", round(group1_hpd_upper, 2), "]",
        sep_by,
        "[", round(group2_hpd_lower, 2), ",", round(group2_hpd_upper, 2), "]"
      )
    )

  std_comparisons <- std_solution |>
    filter(group %in% c(group1, group2)) |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      group1_std = est.std[group == group1],
      group2_std = est.std[group == group2],
      .groups = 'drop'
    )

  comparison_table <- sig_diffs |>
    inner_join(group_comparisons, by = c("lhs", "op", "rhs")) |>
    left_join(std_comparisons, by = c("lhs", "op", "rhs")) |>
    left_join(group1_order, by = c("lhs", "op", "rhs")) |>
    mutate(
      comparison_unstd = paste0(round(group1_est, 2), sep_by, round(group2_est, 2)),
      comparison_std = paste0(round(group1_std, 2), sep_by, round(group2_std, 2)),
    ) |>
    arrange(order) |>
    select(lhs, op, rhs, comparison_unstd, comparison_std, confint_unstd,
           group1_est, group2_est, group1_std, group2_std,
           posterior_mean_diff, credible_interval, excludes_zero, excludes_rope,
           rope_lower, rope_upper) |>
    rename(est = comparison_unstd, std = comparison_std, significant = excludes_zero) |>
    distinct(lhs, op, rhs, .keep_all = TRUE)

  return(comparison_table)
}

#' Generate frequentist model comparison table
#'
#' Creates a comparison table for frequentist structural equation models, including
#' parameter estimates, confidence intervals, and significance tests for group differences.
#' This function extracts and compares parameter estimates between two groups in a
#' frequentist SEM framework, incorporating Wald tests for group differences.
#'
#' @param fit A lavaan object containing the fitted SEM model with multiple groups
#' @param alpha Numeric significance level for determining significant differences.
#'   Default is 0.05
#' @param group1 Character string specifying the first group for comparison.
#'   Must match a group label in the fitted model
#' @param group2 Character string specifying the second group for comparison.
#'   Must match a group label in the fitted model
#' @param sep_by Character separator used in the comparison columns to distinguish
#'   between group1 and group2 values. Default is "|"
#'
#' @return A data frame containing the comparison table
#'
#' @importFrom lavaan lavInspect standardizedSolution
#' @importFrom dplyr filter mutate distinct group_by summarise arrange select rename inner_join left_join row_number
#' @importFrom rlang .data
#'
#' @keywords internal
#' @noRd
get_comparison_table <- function(fit, alpha = 0.05, group1 = "", group2 = "", sep_by = "|") {
  sig_diffs <- get_est_differences(fit = fit, alpha = alpha, group1 = group1, group2 = group2)

  if (nrow(sig_diffs) == 0) {
    return(data.frame())
  }

  params <- get_params_with_group_names(fit)

  # Get standardized solutions
  std_solution <- standardizedSolution(fit)
  std_solution <- std_solution |>
    filter(op %in% c("=~", "~1", "~~")) |>
    mutate(group = ifelse(group %in% 1:length(lavInspect(fit, "group.label")),
                          lavInspect(fit, "group.label")[group],
                          group))

  group1_order <- params |>
    filter(group == group1) |>
    distinct(lhs, op, rhs) |>
    mutate(order = row_number())

  # Get unstandardized comparisons with confidence intervals (if available)
  group_comparisons <- params |>
    filter(group %in% c(group1, group2)) |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      group1_est = est[group == group1],
      group2_est = est[group == group2],
      group1_ci_lower = if ("ci.lower" %in% names(params)) ci.lower[group == group1] else NA_real_,
      group1_ci_upper = if ("ci.upper" %in% names(params)) ci.upper[group == group1] else NA_real_,
      group2_ci_lower = if ("ci.lower" %in% names(params)) ci.lower[group == group2] else NA_real_,
      group2_ci_upper = if ("ci.upper" %in% names(params)) ci.upper[group == group2] else NA_real_,
      .groups = 'drop'
    )

  # Get standardized comparisons with confidence intervals (if available)
  std_comparisons <- std_solution |>
    filter(group %in% c(group1, group2)) |>
    group_by(lhs, op, rhs) |>
    filter(dplyr::n() == 2) |>
    summarise(
      group1_std = est.std[group == group1],
      group2_std = est.std[group == group2],
      group1_std_ci_lower = if ("ci.lower" %in% names(std_solution)) ci.lower[group == group1] else NA_real_,
      group1_std_ci_upper = if ("ci.upper" %in% names(std_solution)) ci.upper[group == group1] else NA_real_,
      group2_std_ci_lower = if ("ci.lower" %in% names(std_solution)) ci.lower[group == group2] else NA_real_,
      group2_std_ci_upper = if ("ci.upper" %in% names(std_solution)) ci.upper[group == group2] else NA_real_,
      .groups = 'drop'
    )

  comparison_table <- sig_diffs |>
    inner_join(group_comparisons, by = c("lhs", "op", "rhs")) |>
    left_join(std_comparisons, by = c("lhs", "op", "rhs")) |>
    left_join(group1_order, by = c("lhs", "op", "rhs")) |>
    mutate(
      comparison_unstd = paste0(round(group1_est, 2), sep_by, round(group2_est, 2)),
      comparison_std = if (!all(is.na(group1_std)) & !all(is.na(group2_std))) {
        paste0(round(group1_std, 2), sep_by, round(group2_std, 2))
      } else {
        NA_character_
      },
      # Add confidence intervals (handle missing values)
      confint_unstd = if (!all(is.na(group1_ci_lower)) & !all(is.na(group1_ci_upper)) &
                          !all(is.na(group2_ci_lower)) & !all(is.na(group2_ci_upper))) {
        paste0(
          "[", round(group1_ci_lower, 2), ",", round(group1_ci_upper, 2), "]",
          sep_by,
          "[", round(group2_ci_lower, 2), ",", round(group2_ci_upper, 2), "]"
        )
      } else {
        NA_character_
      },
      confint_std = if (!all(is.na(group1_std_ci_lower)) & !all(is.na(group1_std_ci_upper)) &
                        !all(is.na(group2_std_ci_lower)) & !all(is.na(group2_std_ci_upper))) {
        paste0(
          "[", round(group1_std_ci_lower, 2), ",", round(group1_std_ci_upper, 2), "]",
          sep_by,
          "[", round(group2_std_ci_lower, 2), ",", round(group2_std_ci_upper, 2), "]"
        )
      } else {
        NA_character_
      }
    ) |>
    arrange(order) |>
    select(lhs, op, rhs, comparison_unstd, comparison_std, confint_unstd, confint_std,
           group1_est, group2_est, group1_std, group2_std,
           group1_ci_lower, group1_ci_upper, group2_ci_lower, group2_ci_upper,
           group1_std_ci_lower, group1_std_ci_upper, group2_std_ci_lower, group2_std_ci_upper,
           p_value, significant) |>
    rename(pvalue = p_value, est = comparison_unstd, std = comparison_std) |>
    distinct(lhs, op, rhs, .keep_all = TRUE)

  return(comparison_table)
}

#' Extract parameters with group names from lavaan model
#'
#' Extracts parameter estimates from a lavaan model and replaces numeric group indices
#' with meaningful group labels. This function simplifies working with multi-group
#' SEM models by providing human-readable group identifiers in the parameter table.
#'
#' @param fit A lavaan object containing the fitted SEM model
#'
#' @return A data frame containing parameter estimates

#' @examples
#' \dontrun{
#' # Assuming 'fit' is a multi-group lavaan model
#' params <- get_params_with_group_names(fit)
#'
#' # View parameters with group names
#' head(params)
#'
#' # Filter parameters for a specific group
#' params_group1 <- params |> filter(group == "Group1")
#' }
#'
#' @importFrom lavaan lavInspect parameterEstimates
#' @importFrom dplyr filter mutate
#' @importFrom stats setNames
#' @keywords internal
#' @noRd
get_params_with_group_names <- function(fit) {
  group_info <- lavInspect(fit, "group.label")
  group_mapping <- setNames(group_info, 1:length(group_info))

  params <- parameterEstimates(fit) |>
    filter(op %in% c("=~", "~1", "~~")) |>
    mutate(group = ifelse(group %in% names(group_mapping),
                          group_mapping[as.character(group)],
                          group))
  return(params)
}

#' Update tidysem object labels with statistical information
#'
#' This function updates the edge and node labels in a tidysem object with
#' formatted statistical information including estimates, significance stars,
#' and confidence intervals based on user specifications.
#'
#' @param tidysem_obj A tidysem object containing edges and optionally nodes
#'   data frames with parameter estimates.
#' @param standardized Logical. Whether to include standardized estimates
#'   in the labels. Default is FALSE.
#' @param unstandardized Logical. Whether to include unstandardized estimates
#'   in the labels. Default is TRUE.
#' @param p_val Logical. Whether to include significance stars (*) for
#'   significant parameters (p < 0.05). Default is TRUE.
#' @param conf_int Logical. Whether to include confidence intervals in the
#'   labels. Default is FALSE.
#'
#' @return The modified tidysem object with updated `label` columns in the
#'   edges and nodes data frames containing formatted statistical information.
#'
#' @importFrom dplyr case_when mutate
#'
#'
#' @examples
#' \dontrun{
#' # Update labels with standardized estimates and significance stars
#' tidysem_obj <- update_tidysem_labels(tidysem_obj,
#'                                      standardized = TRUE,
#'                                      p_val = TRUE)
#'
#' # Show only unstandardized estimates with confidence intervals
#' tidysem_obj <- update_tidysem_labels(tidysem_obj,
#'                                      standardized = FALSE,
#'                                      unstandardized = TRUE,
#'                                      conf_int = TRUE)
#' }
#'
#' @keywords internal
#' @noRd
update_tidysem_labels <- function(tidysem_obj, standardized = FALSE, unstandardized = TRUE,
                                  p_val = TRUE, conf_int = FALSE) {

  create_label <- function(row, standardized, unstandardized, p_val, conf_int) {
    # Check which columns exist
    has_std <- all(c("est_sig_std", "est_std", "confint_std") %in% names(row))
    has_unstd <- all(c("est_sig", "est", "confint") %in% names(row))

    if (standardized == FALSE & unstandardized == FALSE) {
      if (conf_int & has_unstd) {
        return(ifelse(!is.na(row$confint), row$confint, ""))
      } else {
        return("")
      }
    }

    if (standardized == TRUE & !has_std) {
      # Fall back to unstandardized if available
      if (unstandardized == FALSE) {
        unstandardized <- TRUE
      }
      standardized <- FALSE
    }

    if (standardized == TRUE & unstandardized == TRUE) {
      if (p_val & conf_int) {
        return(paste0(row$est_sig, " (", row$est_sig_std, ")\n", row$confint))
      } else if (p_val & !conf_int) {
        return(paste0(row$est_sig, " (", row$est_sig_std, ")"))
      } else if (!p_val & conf_int) {
        return(paste0(row$est, " (", row$est_std, ")\n", row$confint))
      } else {
        return(paste0(row$est, " (", row$est_std, ")"))
      }
    }

    if (standardized == TRUE & unstandardized == FALSE) {
      if (p_val & conf_int) {
        return(paste0(row$est_sig_std, "\n", row$confint_std))
      } else if (p_val & !conf_int) {
        return(row$est_sig_std)
      } else if (!p_val & conf_int) {
        return(paste0(row$est_std, "\n", row$confint_std))
      } else {
        return(as.character(row$est_std))
      }
    }

    if (standardized == FALSE & unstandardized == TRUE) {
      if (p_val & conf_int) {
        return(paste0(row$est_sig, "\n", row$confint))
      } else if (p_val & !conf_int) {
        return(row$est_sig)
      } else if (!p_val & conf_int) {
        return(paste0(row$est, "\n", row$confint))
      } else {
        return(as.character(row$est))
      }
    }

    return("")
  }

  if ("edges" %in% names(tidysem_obj)) {
    tidysem_obj$edges$label <- apply(tidysem_obj$edges, 1, function(row) {
      create_label(as.list(row), standardized, unstandardized, p_val, conf_int)
    })
  }

  if ("nodes" %in% names(tidysem_obj) && !is.null(tidysem_obj$nodes)) {
    if (nrow(tidysem_obj$nodes) > 0 && all(c("est_sig", "est", "confint") %in% names(tidysem_obj$nodes))) {
      tidysem_obj$nodes$label <- apply(tidysem_obj$nodes, 1, function(row) {
        create_label(as.list(row), standardized, unstandardized, p_val, conf_int)
      })
    }
  }

  return(tidysem_obj)
}

#' Update tidysem object labels with Bayesian statistical information
#'
#' This function updates edge and node labels in a tidysem object with
#' formatted Bayesian statistical information including estimates, significance
#' indicators based on HPD intervals, and credible intervals.
#'
#' @param tidysem_obj A tidysem object containing edges and optionally nodes
#'   data frames with parameter estimates.
#' @param blavaan_fit A blavaan model object from which to extract HPD intervals.
#' @param standardized Logical. Whether to include standardized estimates
#'   in the labels. Default is FALSE.
#' @param unstandardized Logical. Whether to include unstandardized estimates
#'   in the labels. Default is TRUE.
#' @param p_val Logical. Whether to include significance indicators (*) for
#'   parameters whose HPD intervals do not contain zero. Default is FALSE.
#' @param conf_int Logical. Whether to include HPD credible intervals in the
#'   labels. Default is FALSE.
#' @param ci_level Numeric. The credible interval level for HPD intervals.
#'   Default is 0.95 (95% credible intervals).
#'
#' @return The modified tidysem object with updated `label` columns in the
#'   edges and nodes data frames containing formatted Bayesian statistical
#'   information.
#' @examples
#' \dontrun{
#' # Fit Bayesian SEM with blavaan
#' fit <- bsem(model, data = mydata)
#'
#' # Convert to tidysem format
#' tidysem_obj <- tidy(fit)
#'
#' # Update labels with HPD intervals
#' tidysem_obj <- update_tidysem_labels_bayes(tidysem_obj, fit,
#'                                            conf_int = TRUE,
#'                                            ci_level = 0.89)
#'
#' # Show standardized estimates with significance based on HPD
#' tidysem_obj <- update_tidysem_labels_bayes(tidysem_obj, fit,
#'                                            standardized = TRUE,
#'                                            p_val = TRUE)
#' }
#' @importFrom dplyr mutate case_when select left_join
#' @importFrom blavaan blavInspect
#' @importFrom rlang .data
#' @keywords internal
#' @noRd
update_tidysem_labels_bayes <- function(tidysem_obj, blavaan_fit,
                                        standardized = FALSE, unstandardized = TRUE,
                                        p_val = FALSE, conf_int = FALSE,
                                        ci_level = 0.95) {

  hpd_intervals <- as.data.frame(blavaan::blavInspect(blavaan_fit, "hpd", level = ci_level))
  hpd_intervals$parameter <- rownames(hpd_intervals)

  group_info <- blavaan::blavInspect(blavaan_fit, "group.label")

  convert_to_hpd_name <- function(lhs, op, rhs, group, label = NULL) {
    group_num <- match(group, group_info)
    if (is.na(group_num)) group_num <- 1

    if (!is.null(label) && label != "" && grepl("^\\.p[0-9]+\\.$", label)) {
      # Has a lavaan label (like .p2., .p3., etc.)
      base_name <- label

      if (group_num == 1) {
        return(base_name)
      } else {
        possible_names <- c(
          paste0(base_name, "..", group_num),  # .p2..1
          paste0(base_name, ".g", group_num),  # .p2.g2
          base_name  # Also try without suffix as fallback
        )
        return(possible_names)
      }
    } else if (op == "~~") {
      base_name <- paste0(lhs, "..", rhs)

      if (group_num == 1) {
        return(base_name)
      } else {
        return(paste0(base_name, ".g", group_num))
      }
    } else if (op == "~1" && lhs %in% c("visual", "textual", "speed")) {
      if (group_num > 1) {
        return(paste0(lhs, ".1.g", group_num))
      }
    }

    return(NULL)
  }

  add_significance_stars <- function(est_values, lower_values, upper_values) {
    significant <- !is.na(lower_values) & !is.na(upper_values) &
      (lower_values > 0 | upper_values < 0)
    ifelse(significant, paste0(est_values, "*"), est_values)
  }

  add_significance_indicator <- function(lower_values, upper_values) {
    significant <- !is.na(lower_values) & !is.na(upper_values) &
      (lower_values > 0 | upper_values < 0)
    ifelse(significant, 0, NA_real_)  # 0 for significant, NA for not
  }

  hpd_names <- hpd_intervals$parameter
  is_blavaan_format <- any(grepl("^\\.p[0-9]+\\.$", hpd_names))

  if (is_blavaan_format) {
    edges_with_hpd <- tidysem_obj$edges |>
      rowwise() |>
      mutate(
        hpd_name = {
          if (!is.na(lavaan_label) && lavaan_label != "" && grepl("^\\.p[0-9]+\\.$", lavaan_label)) {
            label <- lavaan_label
            group_num <- match(group, group_info)
            if (is.na(group_num)) group_num <- 1

            if (group_num == 1) {
              # Group 1
              label
            } else {
              # Group 2 - try different patterns
              name1 <- paste0(label, "..", group_num)
              name2 <- paste0(label, ".g", group_num)

              if (name1 %in% hpd_names) name1
              else if (name2 %in% hpd_names) name2
              else label  # fallback
            }
          } else if (op == "~~") {
            # Variance or covariance
            base_name <- paste0(lhs, "..", rhs)
            group_num <- match(group, group_info)
            if (is.na(group_num)) group_num <- 1

            if (group_num == 1) {
              base_name
            } else {
              paste0(base_name, ".g", group_num)
            }
          } else {
            NA_character_
          }
        }
      ) |>
      ungroup() |>
      left_join(hpd_intervals |> select(parameter, lower, upper),
                by = c("hpd_name" = "parameter")) |>
      select(-hpd_name)

  } else {
    edges_with_hpd <- tidysem_obj$edges |>
      mutate(
        param_name = case_when(
          op == "=~" ~ paste0(lhs, op, rhs),
          op == "~~" ~ paste0(lhs, op, rhs),
          op == "~1" ~ paste0(lhs, op),
          TRUE ~ paste0(lhs, op, rhs)
        )
      ) |>
      left_join(hpd_intervals |> select(parameter, lower, upper),
                by = c("param_name" = "parameter")) |>
      select(-param_name)
  }

  tidysem_obj$edges <- edges_with_hpd |>
    mutate(
      confint = ifelse(!is.na(lower),
                       paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                       NA_character_),
      confint_std = confint,

      pval = add_significance_indicator(lower, upper),

      est_sig = if (p_val) add_significance_stars(est, lower, upper) else est,
      est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else est_std
    ) |>
    select(-lower, -upper)

  tidysem_obj$edges <- tidysem_obj$edges |>
    mutate(
      label = case_when(
        # Case 1: Both standardized and unstandardized are FALSE
        standardized == FALSE & unstandardized == FALSE ~
          if (conf_int) {
            # Show unstandardized confidence interval without "\n"
            confint
          } else {
            ""
          },

        # Case 2: Show both standardized and unstandardized
        standardized == TRUE & unstandardized == TRUE ~
          if (p_val & conf_int) {
            ifelse(!is.na(confint),
                   paste0(est_sig, " (", est_sig_std, ")\n", confint),
                   paste0(est_sig, " (", est_sig_std, ")"))
          } else if (p_val & !conf_int) {
            paste0(est_sig, " (", est_sig_std, ")")
          } else if (!p_val & conf_int) {
            ifelse(!is.na(confint),
                   paste0(est, " (", est_std, ")\n", confint),
                   paste0(est, " (", est_std, ")"))
          } else {
            paste0(est, " (", est_std, ")")
          },

        # Case 3: Show only standardized
        standardized == TRUE & unstandardized == FALSE ~
          if (p_val & conf_int) {
            ifelse(!is.na(confint_std),
                   paste0(est_sig_std, "\n", confint_std),
                   est_sig_std)
          } else if (p_val & !conf_int) {
            est_sig_std
          } else if (!p_val & conf_int) {
            ifelse(!is.na(confint_std),
                   paste0(est_std, "\n", confint_std),
                   est_std)
          } else {
            as.character(est_std)
          },

        # Case 4: Show only unstandardized
        standardized == FALSE & unstandardized == TRUE ~
          if (p_val & conf_int) {
            ifelse(!is.na(confint),
                   paste0(est_sig, "\n", confint),
                   est_sig)
          } else if (p_val & !conf_int) {
            est_sig
          } else if (!p_val & conf_int) {
            ifelse(!is.na(confint),
                   paste0(est, "\n", confint),
                   est)
          } else {
            as.character(est)
          },

        # Fallback (shouldn't be reached)
        TRUE ~ ""
      )
    )

  if (!is.null(tidysem_obj$nodes) && "est" %in% names(tidysem_obj$nodes)) {

    if (is_blavaan_format) {
      nodes_with_hpd <- tidysem_obj$nodes |>
        mutate(
          # For latent intercepts in group 2
          hpd_name = ifelse(name %in% c("visual", "textual", "speed") &
                              match(group, group_info) > 1,
                            paste0(name, ".1.g", match(group, group_info)),
                            NA_character_)
        ) |>
        left_join(hpd_intervals |> select(parameter, lower, upper),
                  by = c("hpd_name" = "parameter")) |>
        select(-hpd_name)
    } else {
      nodes_with_hpd <- tidysem_obj$nodes |>
        mutate(
          param_name = paste0(name, "~1")
        ) |>
        left_join(hpd_intervals |> select(parameter, lower, upper),
                  by = c("param_name" = "parameter")) |>
        select(-param_name)
    }

    tidysem_obj$nodes <- nodes_with_hpd |>
      mutate(
        confint = ifelse(!is.na(lower),
                         paste0("[", round(lower, 2), ", ", round(upper, 2), "]"),
                         NA_character_),
        confint_std = confint,

        pval = add_significance_indicator(lower, upper),

        est_sig = if (p_val) add_significance_stars(est, lower, upper) else est,
        est_sig_std = if (p_val) add_significance_stars(est_std, lower, upper) else est_std
      ) |>
      select(-lower, -upper)

    tidysem_obj$nodes <- tidysem_obj$nodes |>
      mutate(
        label = case_when(
          # Case 1: Both standardized and unstandardized are FALSE
          standardized == FALSE & unstandardized == FALSE ~
            if (conf_int) {
              # Show unstandardized confidence interval without "\n"
              confint
            } else {
              ""
            },

          # Case 2: Show both standardized and unstandardized
          standardized == TRUE & unstandardized == TRUE ~
            if (p_val & conf_int) {
              ifelse(!is.na(confint),
                     paste0(est_sig, " (", est_sig_std, ")\n", confint),
                     paste0(est_sig, " (", est_sig_std, ")"))
            } else if (p_val & !conf_int) {
              paste0(est_sig, " (", est_sig_std, ")")
            } else if (!p_val & conf_int) {
              ifelse(!is.na(confint),
                     paste0(est, " (", est_std, ")\n", confint),
                     paste0(est, " (", est_std, ")"))
            } else {
              paste0(est, " (", est_std, ")")
            },

          # Case 3: Show only standardized
          standardized == TRUE & unstandardized == FALSE ~
            if (p_val & conf_int) {
              ifelse(!is.na(confint_std),
                     paste0(est_sig_std, "\n", confint_std),
                     est_sig_std)
            } else if (p_val & !conf_int) {
              est_sig_std
            } else if (!p_val & conf_int) {
              ifelse(!is.na(confint_std),
                     paste0(est_std, "\n", confint_std),
                     est_std)
            } else {
              as.character(est_std)
            },

          # Case 4: Show only unstandardized
          standardized == FALSE & unstandardized == TRUE ~
            if (p_val & conf_int) {
              ifelse(!is.na(confint),
                     paste0(est_sig, "\n", confint),
                     est_sig)
            } else if (p_val & !conf_int) {
              est_sig
            } else if (!p_val & conf_int) {
              ifelse(!is.na(confint),
                     paste0(est, "\n", confint),
                     est)
            } else {
              as.character(est)
            },

          # Fallback (shouldn't be reached)
          TRUE ~ ""
        )
      )
  }

  return(tidysem_obj)
}

Try the ggsem package in your browser

Any scripts or data that you put into this service are public.

ggsem documentation built on Oct. 16, 2026, 1:06 a.m.