# ==== HELPER FUNCTIONS (GLOBAL IN SERVER) ====
# CR AVE & HTMT ====
calc_ave_cr <- function(fit) {
  std <- lavaan::standardizedSolution(fit)
  
  loading <- std[std$op == "=~", c("lhs","rhs","est.std")]
  loading$err <- 1 - loading$est.std^2
  
  result <- lapply(split(loading, loading$lhs), function(x) {
    lambda2 <- x$est.std^2
    theta   <- x$err
    
    AVE <- sum(lambda2) / (sum(lambda2) + sum(theta))
    CR  <- (sum(x$est.std))^2 /
      ((sum(x$est.std))^2 + sum(theta))
    
    c(AVE = AVE, CR = CR)
  })
  
  as.data.frame(do.call(rbind, result))
}
calc_htmt <- function(data, model_syntax,
                      missing = "listwise",
                      ordered = NULL,
                      absolute = TRUE) {
  
  ## -------------------------------------------------
  ## 1. Parse lavaan model syntax
  ## -------------------------------------------------
  lines <- unlist(strsplit(model_syntax, "\n"))
  fac_lines <- lines[grepl("=~", lines)]
  
  blocks <- lapply(fac_lines, function(x) {
    trimws(unlist(strsplit(strsplit(x, "=~")[[1]][2], "\\+")))
  })
  names(blocks) <- trimws(sub("=~.*", "", fac_lines))
  
  # Merge items for factors defined across multiple lines (duplicate factor names)
  unique_factors <- unique(names(blocks))
  merged_blocks <- lapply(unique_factors, function(f) {
    unique(unlist(blocks[names(blocks) == f]))
  })
  names(merged_blocks) <- unique_factors
  blocks <- merged_blocks
  
  ## -------------------------------------------------
  ## 2. Lavcor
  ## -------------------------------------------------
  R <- lavaan::lavCor(
    data,
    missing = missing,
    ordered = ordered,
    output  = "cor"
  )
  
  if (absolute) R <- abs(R)
  
  ## -------------------------------------------------
  ## 3. Indicator check 
  ## -------------------------------------------------
  blocks <- lapply(blocks, function(x) intersect(x, colnames(R)))
  
  ## -------------------------------------------------
  ## 4. calculate HTMT2 (GEOMETRIC MEAN)
  ## -------------------------------------------------
  k <- length(blocks)
  htmt <- matrix(NA, k, k, dimnames = list(names(blocks), names(blocks)))
  
  for (i in seq_len(k)) {
    for (j in seq_len(k)) {
      
      if (i == j) {
        htmt[i, j] <- 1
        next
      }
      
      Xi <- blocks[[i]]
      Xj <- blocks[[j]]
      
      # heterotrait correlations
      r_het <- R[Xi, Xj]
      
      # monotrait correlations
      r_mono_i <- R[Xi, Xi][lower.tri(R[Xi, Xi])]
      r_mono_j <- R[Xj, Xj][lower.tri(R[Xj, Xj])]
      
      htmt[i, j] <-
        exp(mean(log(r_het))) /
        sqrt(
          exp(mean(log(r_mono_i))) *
            exp(mean(log(r_mono_j)))
        )
    }
  }
  
  htmt
}
# ====END of CR AVE and HTMT ====
server_cfa <- function(input, output, session) {
  library(lavaan)
  library(semPlot)
  library(readxl)
  library(psych)
  library(tibble)
  library(semptools)
  # library(semTools)
  library(data.table)
  library(kableExtra)
  
  data_user <- reactive({
    if (input$data_source == "bfi") {
      df <- psych::bfi %>% dplyr::select(A1:O5) %>% tibble::rownames_to_column("id_auto")
    }
    if (input$data_source == "HolzingerSwineford1939") {
      df <- lavaan::HolzingerSwineford1939 %>% dplyr::select(x1:x9) %>% tibble::rownames_to_column("id_auto")
    }
    
    if (input$data_source == "upload") {
      req(input$datafile)
      ext <- tolower(tools::file_ext(input$datafile$name))
      showModal(modalDialog(title = NULL, "Reading Your File, Please wait...", footer = NULL, easyClose = FALSE))
      df <- switch(
        ext,
        "csv"  = data.table::fread(input$datafile$datapath,data.table = FALSE),
        "xls"  = readxl::read_excel(input$datafile$datapath),
        "xlsx" = readxl::read_excel(input$datafile$datapath),
        "sav"  = haven::read_sav(input$datafile$datapath),
        "rds"  = readRDS(input$datafile$datapath),
        stop("Unsupported file type. Please upload CSV, Excel, SPSS (.sav), or RDS file.")
      )
      removeModal()
      df <- df %>% mutate(across(everything(), ~ifelse(.x == "", NA, .x)),
                          id_auto = paste0("id_", sprintf("%04d", 1:n())))
    }
    return(df)
  })
  
  output$id_select_ui <- renderUI({
    req(data_user())
    selectizeInput("id_cols", "ID columns (optional)", choices = names(data_user()), multiple = TRUE,
                   selected = names(data_user())[grepl("id", names(data_user()), ignore.case = TRUE)])
  })
  
  output$var_select_ui <- renderUI({
    req(data_user())
    all_vars <- names(data_user())
    id_cols <- input$id_cols
    choices <- setdiff(all_vars, id_cols)
    selectInput("selected_vars", "Select Variables (used for CFA):", choices = choices, multiple = TRUE, selected = choices)
  })
  
  output$data_preview <- renderDT({
    req(data_user(), input$selected_vars)
    df <- data_user()[, input$selected_vars, drop = FALSE]
    # Identifikasi kolom numeric 
    numeric_cols <- which(sapply(df, is.numeric))
    #df <- df %>% mutate(across(where(is.numeric), ~ round(.x, 3)))
    datatable(df,
              options = list(scrollX = TRUE, dom = 'rtp', pageLength = 25), 
              rownames = T) %>% 
      formatRound(columns = numeric_cols, digits = 2)
  }, server = TRUE)
  
  
  # ===== CFA MODEL =====
  output$cfa_model_ui <- renderUI({
    model_text <- switch(input$data_source,
                         "bfi" = "A =~ A1 + A2 + A3 + A4 + A5\nC =~ C1 + C2 + C3 + C4 + C5\nE =~ E1 + E2 + E3 + E4 + E5\nN =~ N1 + N2 + N3 + N4 + N5\nO =~ O1 + O2 + O3 + O4 + O5",
                         "HolzingerSwineford1939" = "Visual =~ x1 + x2 + x3\nTextual =~ x4 + x5 + x6\nSpeed =~ x7 + x8 + x9\nG =~ Visual+Textual+Speed",
                         "# Enter your lavaan model syntax here\n# Example:\n# factor1 =~ item1 + item2 + item3\n# factor2 =~ item4 + item5 + item6"
    )
    
    tags$textarea(id = "cfa_model_text", rows = 8, style = "width:100%; font-family: monospace; font-size:11.5px", model_text)
  })
  
  # ---- CFA storage ----
  cfa_fit_list <- reactiveVal(list())
  cfa_current_id <- reactiveVal(NULL)
  cfa_model_counter <- reactiveVal(0)
  run_lavaan <- function(model_text, data, estimator = "ML", missing = "listwise") {
    tryCatch({
      data_numeric <- data %>% dplyr::select(where(is.numeric))
      
      if (estimator %in% c("DWLS", "WLSMV","MLR","GLS", "ULS" )) 
      {
        lavaan::cfa(model = model_text, data = data_numeric, estimator = estimator, ov.order = "model",
                    missing = missing)
      } else {
        if (missing == "fiml") {
          lavaan::cfa(model = model_text, data = data_numeric, estimator = estimator, ov.order = "model", missing = "fiml")
        } else if (missing == "pairwise") {
          lavaan::cfa(model = model_text, data = data_numeric, estimator = estimator, ov.order = "model", missing = "pairwise")
        } else {
          lavaan::cfa(model = model_text, data = data_numeric, estimator = estimator, ov.order = "model", missing = "listwise")
        }
      }
    }, error = function(e) {
      showNotification(paste("Model error:", e$message), type = "error")
      return(NULL)
    })
  }
  observeEvent(input$run_cfa, {
    req(data_user(), input$cfa_model_text, input$selected_vars)
    updateTabsetPanel(session, "main_tab_cfa", selected = "fit_tab_cfa")
  })
  # Main run CFA
  observeEvent(input$run_cfa, {
    req(data_user(), input$cfa_model_text, input$selected_vars)
    
    if (nchar(trimws(input$cfa_model_text)) == 0) {
      showNotification("Please enter a CFA model", type = "error")
      return()
    }
    
    df <- data_user()[, input$selected_vars, drop = FALSE]
    df <- df %>% mutate(across(everything(), as.numeric))
    
    showModal(modalDialog("Running CFA ...", footer = NULL))
    
    fit <- run_lavaan(
      model_text = input$cfa_model_text, 
      data = df, 
      estimator = input$cfa_estimator, 
      missing = input$cfa_missing
    )
    
    removeModal()
    
    if (is.null(fit)) return()
    
    measures <- tryCatch({
      lavaan::fitMeasures(fit, c("chisq","df","pvalue","rmsea","cfi","tli","srmr", "gfi", "agfi", "nfi", "nnfi"))
    }, error = function(e) {
      c(chisq = NA, df = NA, pvalue = NA, rmsea = NA, cfi = NA, tli = NA, srmr = NA, gfi = NA, agfi = NA, nfi = NA, nnfi = NA)
    })
    
    
    # AVE <- tryCatch({
    #   semTools::AVE(object = fit)
    #   }, error = function(e) {
    #   '<NA>'
    # })
    # RelSEM <- tryCatch({
    #   semTools::compRelSEM(object = fit)
    # }, error = function(e) {
    #   '<NA>'
    # })
    # HTMT <- tryCatch({
    #   semTools::htmt(model = input$cfa_model_text, data = df)
    # }, error = function(e) {
    #   '<NA>'
    # })
    
    ave_cr <- tryCatch({
      calc_ave_cr(fit)
    }, error = function(e) {
      NA
    })
    AVE    <- setNames(ave_cr$AVE, rownames(ave_cr))
    RelSEM <- setNames(ave_cr$CR,  rownames(ave_cr))
    HTMT <- tryCatch({
      calc_htmt(df, input$cfa_model_text)
    }, error = function(e) {
      NA
    })
    
    scoreCfa <- tryCatch({
      lavaan::predict(fit)
    }, error = function(e) {
      '<NA>'
    })
    
    current_count <- cfa_model_counter() + 1
    cfa_model_counter(current_count)
    new_id <- paste0("Model_", current_count)
    
    cur <- cfa_fit_list()
    
    # Auto-generate modification note
    auto_note <- ""
    if (length(cur) == 0) {
      auto_note <- "Initial Model"
    } else {
      last_model <- cur[[length(cur)]]
      old_syntax <- last_model$spec
      new_syntax <- input$cfa_model_text
      
      old_lines <- trimws(unlist(strsplit(old_syntax, "\n")))
      new_lines <- trimws(unlist(strsplit(new_syntax, "\n")))
      old_lines <- old_lines[old_lines != ""]
      new_lines <- new_lines[new_lines != ""]
      
      added <- setdiff(new_lines, old_lines)
      removed <- setdiff(old_lines, new_lines)
      
      changes <- c()
      if (length(added) > 0) changes <- c(changes, paste("Added:", paste(added, collapse = ", ")))
      if (length(removed) > 0) changes <- c(changes, paste("Removed:", paste(removed, collapse = ", ")))
      
      if (length(changes) == 0) {
        auto_note <- "Modified formatting only"
      } else {
        auto_note <- paste(changes, collapse = " | ")
      }
    }
    
    cur[[new_id]] <- list(
      id = new_id, 
      spec = input$cfa_model_text, 
      fit = fit, 
      measures = measures, 
      ave = AVE,
      relsem = RelSEM,
      htmt = HTMT,
      scoreCfa = as.data.frame(scoreCfa),
      note = auto_note,
      time = Sys.time()
    )
    
    cfa_fit_list(cur)
    cfa_current_id(new_id)
    showNotification(paste0("CFA finished: ", new_id), type = "message")
  })
  
  # ====== Fit Comparison Table =======
  output$fit_comparison <- renderUI({
    req(cfa_fit_list())
    lst <- cfa_fit_list()
    if (length(lst) == 0) return(tags$p("No models run yet. Run CFA to see results."))
    df <- do.call(rbind, lapply(lst, function(x){
      m <- x$measures
      status_info <- get_fit_status(m["chisq"], m["df"], m["pvalue"], 
                                    m["rmsea"], m["cfi"], m["srmr"], m["gfi"])
      data.frame(
        Model = x$id,
        Note = ifelse(is.null(x$note) || x$note == "", "-", x$note),
        chisq = round(m["chisq"], 2),
        df = m["df"],
        chisq_df_ratio = ifelse(is.na(m["chisq"]), NA, sprintf("%.2f",m["chisq"]/m["df"])),
        pvalue = ifelse(is.na(m["pvalue"]), NA,
                        sprintf("%.3f", m["pvalue"])),
        RMSEA = ifelse(is.na(m["rmsea"]), NA, sprintf("%.3f", m["rmsea"])),
        CFI = ifelse(is.na(m["cfi"]), NA, sprintf("%.3f", m["cfi"])),
        TLI = ifelse(is.na(m["tli"]), NA, sprintf("%.3f", m["tli"])),
        GFI = ifelse(is.na(m["gfi"]), NA, sprintf("%.3f", m["gfi"])),
        SRMR = ifelse(is.na(m["srmr"]), NA, sprintf("%.3f", m["srmr"])),
        NFI = ifelse(is.na(m["nfi"]), NA, sprintf("%.3f", m["nfi"])),
        Status = status_info$status,
        
        StatusColor = status_info$color,  # tetap: warna background status
        stringsAsFactors = FALSE
      )
    }))
    
    # Add Action column
    df$Action <- sapply(seq_len(nrow(df)), function(i) {
      as.character(
        actionButton(
          inputId = paste0("del_model_btn_", i),
          label = icon("trash"),
          class = "btn-danger btn-sm",
          style = "font-size: 11px !important; padding: 2px 10px !important;",
          onclick = paste0('Shiny.setInputValue("del_model_selected", "', df$Model[i], '", {priority: "event"})')
        )
      )
    })
    
    # Buat tabel HTML manual
    column(12,
           tags$div(
             style = "margin-top: 0px; font-size: 13px;",
             tags$b("Fit Model Comparison")),
           # ---Render table DT =====
           datatable(df[, c("Model", "Note", "chisq", "df", "chisq_df_ratio", "pvalue", "RMSEA", "CFI", "TLI", "GFI", "SRMR", "NFI", "Status", "Action")],
                     rownames = FALSE,
                     escape = FALSE,
                     extensions = 'Buttons',
                     options = list(
                       dom = 'Brt',
                       buttons = list(
                         list(
                           extend = 'csv',
                           text = 'Export CSV',
                           filename = paste0('Fit Model Comparison')
                         ),
                         list(
                           extend = 'excel',
                           text = 'Export Excel',
                           filename = paste0('Fit Model Comparison')
                         )),
                       pageLength = 30,
                       columnDefs = list(list(className = 'dt-center', targets = 2:11))
                     )) %>%
             # --- Tetap: style kolom Status dengan warna ---
             formatStyle('chisq_df_ratio', color = styleInterval(c(2,5), c('green','orange','red')), fontWeight = 'bold', textAlign = 'center') %>% 
             # formatStyle('df', color = styleInterval(c(2,5), c('green','orange','red'))[df$chi_sq_ratio], fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('pvalue', color = styleInterval(c(0.05,0.1), c('red','green','green')), fontWeight = 'bold', textAlign = 'center') %>%
             formatStyle('RMSEA', color = styleInterval(c(0.08,0.1), c('green','orange','red')), fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('CFI', color = styleInterval(c(0.88,0.9), c('red','orange','green')), fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('TLI', color = styleInterval(c(0.88,0.9), c('red','orange','green')), fontWeight = 'bold', textAlign = 'center') %>%
             formatStyle('GFI', color = styleInterval(c(0.88,0.9), c('red','orange','green')), fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('SRMR', color = styleInterval(c(0.05,0.08), c('green','orange','red')), fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('NFI', color = styleInterval(c(0.88,0.9), c('red','orange','green')), fontWeight = 'bold', textAlign = 'center') %>% 
             formatStyle('Status', backgroundColor = styleEqual(df$Status, df$StatusColor), fontWeight = 'bold', color = 'black', textAlign = 'center'),
           tags$div(
             style = "margin-top: 0px; font-size: 12px; color: #6c757d;",
             tags$b("Interpretation Guidelines:"),
             tags$ul(
               tags$li(tags$span(style = "color: green;", "Good:"),"χ² < 2 df; pvalue ≥ 0.05; RMSEA < 0.08; CFI/TLI/NFI ≥ 0.9; SRMR ≤ 0.05;"),
               tags$li(tags$span(style = "color: orange;", "Acceptable:"), "2 df ≤ χ² < 5 df; RMSEA < 0.1; CFI/TLI/NFI ≥ 0.89; SRMR ≤ 0.08;"), 
               tags$li(tags$span(style = "color: red;", "Poor:"), "Outside acceptable ranges")
             )
           ),
           tags$hr(),
           column(6,
                  tags$div(
                    style = "margin-top: 0px; font-size: 13px;",
                    tags$b("Average Variance Explained (AVE):"),
                    DTOutput("ave_table")  # Table AVE
                  )), 
           column(6,
                  tags$div(
                    style = "margin-top: 1px; font-size: 13px;",
                    tags$b("Composite Reliability (CR):"),
                    DTOutput("reliability_table"),  # Table Reliability
                  )),
           tags$div(
             style = "margin-top: 0px; font-size: 12px; color: #6c757d;",
             tags$b("Interpretation Guidelines:"),
             tags$ul(
               tags$li(tags$span(style = "color: green;", "Good:"),"AVE ≥ 0.5; CR ≥ 0.7"),
               tags$li(tags$span(style = "color: orange;", "Acceptable:"), "AVE ≥ 0.45; CR ≥ 0.6"), 
               tags$li(tags$span(style = "color: red;", "Poor:"), "Outside acceptable ranges")
             ),
             tags$p(tags$span(style = "color: blue;", "References:"), "Alamer (2025); Hu & Bentler (1999); Kline (2015); & Schumacker & Lomax (2016)")
           ),
           
           conditionalPanel(
             condition = "input.htmt_opt == true",
             tags$hr(),
             tags$div(
               style = "margin-top: 0px; font-size: 13px;",
               tags$b('Heterotrait–Monotrait Ratio (HTMT) for Current Model')), 
             DTOutput("htmt_table"),  
             tags$div(
               style = "margin-top: 0px; font-size: 12px; color: #6c757d;",
               tags$b("Interpretation Guidelines:"),
               tags$ul(
                 tags$li(tags$span(style = "color: green;", "Good discriminant validity:"),"HTMT < 0.7 "),
                 tags$li(tags$span(style = "color: orange;", "Acceptable discriminant validity:"), "HTMT < 0.8"), 
                 tags$li(tags$span(style = "color: red;", "Potential discriminant validity issues:"), "HTMT ≥ 0.8")
               )
             )
           ),
           tags$hr(),
           column(12,
                  tags$div(
                    style = "margin-top: 0px; font-size: 13px;",
                    tags$b('Modification Indices')),    
                  DTOutput("mi_table")
           )
    )
    
  })
  
  # Handle model deletion
  observeEvent(input$del_model_selected, {
    req(input$del_model_selected)
    
    cur_list <- cfa_fit_list()
    model_id <- input$del_model_selected
    
    if (model_id %in% names(cur_list)) {
      cur_list[[model_id]] <- NULL
      cfa_fit_list(cur_list)
      
      # Update current ID
      if (length(cur_list) > 0) {
        cfa_current_id(names(cur_list)[length(cur_list)])
      } else {
        cfa_current_id(NULL)
      }
      
      showNotification(paste("Deleted", model_id), type = "message")
    }
  })
  
  # === Output: HTMT Table =====
  output$htmt_table <- renderDT({
    lst <- cfa_fit_list()
    if (length(lst) == 0) return(NULL)
    
    # Ambil model terakhir
    last_model <- lst[[length(lst)]]
    
    if (!is.null(last_model$htmt) && !identical(last_model$htmt, '<NA>')) {
      # Konversi HTMT matrix ke data frame
      htmt_matrix <- as.matrix(last_model$htmt)
      htmt_df <- as.data.frame(htmt_matrix)
      
      # Tambahkan nama baris sebagai kolom pertama
      htmt_df <- cbind(Variable = rownames(htmt_df), htmt_df)
      rownames(htmt_df) <- NULL
      
      # Identifikasi kolom numeric (HTMT values)
      numeric_cols <- which(sapply(htmt_df, is.numeric))
      
      datatable(htmt_df, rownames = FALSE, extensions = 'Buttons',
                options = list(
                  dom = 'Bt',
                  pageLength = nrow(htmt_df),
                  buttons = list(
                    list(
                      extend = 'csv',
                      text = 'Export CSV',
                      filename = paste0('HTMT')
                    ),
                    list(
                      extend = 'excel',
                      text = 'Export Excel',
                      filename = paste0('HTMT')
                    ))
                )) %>%
        formatStyle(columns = numeric_cols,
                    color = styleInterval(c(0.7, 0.8), c('green', 'orange', 'red'))) %>%
        formatStyle(columns = 'Variable',
                    backgroundColor = '#f8f9fa',
                    fontWeight = 'bold') %>%
        formatRound(columns = numeric_cols, digits = 3)
    } else {
      retu(data.frame(Message = "HTMT not available for the last model"))
    }
  })
  
  # ===== TABEL AVE =====
  output$ave_table <- renderDT({
    lst <- cfa_fit_list()
    if (length(lst) == 0) return(datatable(data.frame(Message = "No models available")))
    
    # Pastikan dplyr tersedia
    if (!require(dplyr, quietly = TRUE)) {
      stop("Package dplyr is required")
    }
    
    ave_data <- lapply(lst, function(x) {
      if (!is.null(x$ave) && !identical(x$ave, '<NA>')) {
        # Konversi AVE ke data frame
        ave_df <- as.data.frame(as.list(round(x$ave, 3)))
        ave_df$Model <- x$id
        return(ave_df)
      } else {
        data.frame(Model = x$id, Note = "AVE not available")
      }
    })
    
    # Gabungkan dengan full_join berantai
    combined_data <- Reduce(function(x, y) {
      dplyr::full_join(x, y, by = intersect(names(x), names(y)))
    }, ave_data)
    
    # Pastikan Model ada di kolom pertama
    if ("Model" %in% names(combined_data)) {
      model_col <- combined_data$Model
      combined_data$Model <- NULL
      combined_data <- data.frame(Model = model_col, combined_data)
    }
    
    datatable(combined_data, extensions = "Buttons",
              rownames = FALSE, 
             
              options = list( pageLength = 15,
                              dom = 'Btp',
                              buttons = list(
                               list(
                                 extend = 'csv',
                                 text = 'Export CSV',
                                 filename = paste0('AVE')
                               ),
                               list(
                                 extend = 'excel',
                                 text = 'Export Excel',
                                 filename = paste0('AVE')
                               )))) %>%
      formatStyle(
        columns = which(!names(combined_data) %in% c("Model", "Note")),
        color = styleInterval(c(0.45, 0.5), c('red', 'orange', 'green'))
      ) %>%
      formatRound(columns = which(sapply(combined_data, is.numeric)), digits = 3)
  })
  
  # ===== Table  Reliability =====
  output$reliability_table <- renderDT({
    lst <- cfa_fit_list()
    if (length(lst) == 0) return(datatable(data.frame(Message = "No models available")))
    
    rel_data <- lapply(lst, function(x) {
      if (!is.null(x$relsem) && !identical(x$relsem, '<NA>')) {
        rel_df <- as.data.frame(as.list(round(x$relsem, 3)), stringsAsFactors = FALSE)
        rel_df$Model <- x$id
        return(rel_df)
      } else {
        data.frame(Model = x$id, Note = "Reliability not available", stringsAsFactors = FALSE)
      }
    })
    
    combined_data <- Reduce(function(x, y) {
      common_cols <- intersect(names(x), names(y))
      if (length(common_cols) > 0) {
        dplyr::full_join(x, y, by = common_cols)
      } else {
        # Jika tidak ada kolom common, gabungkan secara manual
        cbind(x, y[, setdiff(names(y), names(x)), drop = FALSE])
      }
    }, rel_data)
    
    # Pastikan Model ada di kolom pertama
    if ("Model" %in% names(combined_data)) {
      model_col <- combined_data$Model
      combined_data$Model <- NULL
      combined_data <- data.frame(Model = model_col, combined_data, stringsAsFactors = FALSE)
    }
    
    # Identifikasi kolom numeric (Reliability values)
    numeric_cols <- which(sapply(combined_data, is.numeric))
    
    datatable(combined_data, rownames = FALSE, extensions = 'Buttons',
              options = list( pageLength = 15,
                              dom = 'Btp',
                              buttons = list(
                               list(
                                 extend = 'csv',
                                 text = 'Export CSV',
                                 filename = paste0('Composite Realiability')
                               ),
                               list(
                                 extend = 'excel',
                                 text = 'Export Excel',
                                 filename = paste0('Composite Reliability')
                               ))
              )) %>%
      formatStyle(columns = numeric_cols,
                  color = styleInterval(c(0.6, 0.7), c('red', 'orange', 'green'))) %>%
      formatStyle(columns = 'Model',
                  backgroundColor = '#f8f9fa') %>%
      formatRound(columns = numeric_cols, digits = 3)
  })
  
  # ===== Table  FSCORES =====
  output$fscores_cfa <- renderDT({
    req(input$run_cfa)
    lst <- cfa_fit_list()
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    scoreCfa <- cfa_fit_list()[[cur_id]]$scoreCfa
    numeric_cols <- which(sapply(scoreCfa, is.numeric))
    datatable(scoreCfa, rownames = TRUE, extensions = 'Buttons',
              options = list(dom = 'Brtp',
                             buttons = list(
                               list(
                                 extend = 'csv',
                                 text = 'Export CSV',
                                 filename = paste0('Factor Scores')
                               ),
                               list(
                                 extend = 'excel',
                                 text = 'Export Excel',
                                 filename = paste0('Factor Scores')
                               )))) %>%
      formatRound(columns = numeric_cols, digits = 3)
  }, server = FALSE)
  
  # Helper function untuk menentukan status fit ======
  get_fit_status <- function(chisq,df, pvalue, rmsea, cfi, srmr,gfi) {
    if (is.na(chisq)||is.na(rmsea) || is.na(cfi) || is.na(srmr)|| is.na(gfi)) {
      return(list(status = "Unknown", color = "#e2e3e5"))
    }
    if (chisq/df<2 && rmsea < 0.08 && cfi >= 0.9 && gfi >= 0.9 && srmr <= 0.05) {
      return(list(status = "Excellent", color = 'lightgreen'))
    }  else if (pvalue >=0.05 && rmsea < 0.08 && cfi >= 0.9 && gfi >= 0.9 && srmr <= 0.05) {
      return(list(status = "Excellent", color = 'lightgreen'))
    } else if (pvalue < 0.05 && rmsea < 0.08 && cfi >= 0.9 && gfi >= 0.9 && srmr <= 0.05) {
      return(list(status = "Good", color = "#d4edda"))
    } else if  (
      sum(c(chisq/df<2, rmsea < 0.08,cfi >= 0.9, gfi >= 0.9,srmr <= 0.05,pvalue >= 0.05)) >= 3 ||
      sum(c(chisq/df<2, rmsea < 0.08,cfi >= 0.9, gfi >= 0.9,srmr <= 0.05)) >= 3
    ) {
      return(list(status = "Acceptable", color = "#fff3cd"))
    } else {
      return(list(status = "Poor", color = "#f8d7da"))
    }
  }
  
  
  # Loadings table
  output$loadings_table <- renderDT({
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    
    fit <- cfa_fit_list()[[cur_id]]$fit
    
    if (input$cfa_std_est) {
      sol <- tryCatch(lavaan::standardizedSolution(fit), error = function(e) NULL)
    } else {
      sol <- tryCatch(lavaan::parameterEstimates(fit), error = function(e) NULL)
    }
    
    if (is.null(sol)) return(datatable(data.frame(Message = "No parameter estimates available")))
    
    loadings <- sol[sol$op == "=~", ]
    
    if (nrow(loadings) == 0) return(datatable(data.frame(Message = "No factor loadings found")))
    
    if (input$cfa_std_est) {
      display_df <- loadings[, c("lhs","rhs","est.std","pvalue")]
      colnames(display_df) <- c("Factor","Item","StdLoading","pvalue")
      display_df$StdLoading <- round(display_df$StdLoading, 3)
    } else {
      display_df <- loadings[, c("lhs","rhs","est","se","pvalue")]
      colnames(display_df) <- c("Factor","Item","Estimate","SE","pvalue")
      display_df$Estimate <- round(display_df$Estimate, 3)
      display_df$SE <- round(display_df$SE, 3)
    }
    
    display_df$pvalue <- format.pval(display_df$pvalue, digits = 3)
    
    datatable(display_df, extensions = 'Buttons',
              options = list(pageLength = 15, scrollX = TRUE,dom = 'Brtp',
                             buttons = list(
                               list(
                                 extend = 'csv',
                                 text = 'Export CSV',
                                 filename = paste0('Factor Loading')
                               ),
                               list(
                                 extend = 'excel',
                                 text = 'Export Excel',
                                 filename = paste0('Factor Loading')
                               )))) %>% 
      formatStyle(columns = 'StdLoading',
                  fontWeight = 'bold',
                  color = styleInterval(c(0.3, 0.5, 0.7), c('red', 'orange', 'blue', 'green')) )
  }, server = FALSE)
  
  # Variances table
  output$variances_table <- renderDT({
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    
    fit <- cfa_fit_list()[[cur_id]]$fit
    
    if (input$cfa_std_est) {
      sol <- tryCatch(lavaan::standardizedSolution(fit), error = function(e) NULL)
    } else {
      sol <- tryCatch(lavaan::parameterEstimates(fit), error = function(e) NULL)
    }
    
    if (is.null(sol)) return(datatable(data.frame(Message = "No parameter estimates available")))
    
    variances <- sol[sol$op == "~~" & sol$lhs == sol$rhs, ]
    
    if (nrow(variances) == 0) return(datatable(data.frame(Message = "No variances found")))
    
    if (input$cfa_std_est) {
      display_df <- variances[, c("lhs", "est.std", "se", "pvalue")]
      colnames(display_df) <- c("Variable", "StdVariance", "SE", "pvalue")
      display_df$StdVariance <- round(display_df$StdVariance, 3)
      display_df$SE <- round(display_df$SE, 3)
    } else {
      display_df <- variances[, c("lhs", "est", "se", "pvalue")]
      colnames(display_df) <- c("Variable", "Variance", "SE", "pvalue")
      display_df$Variance <- round(display_df$Variance, 3)
      display_df$SE <- round(display_df$SE, 3)
    }
    
    display_df$pvalue <- format.pval(display_df$pvalue, digits = 3)
    
    datatable(display_df, extensions = 'Buttons',
              options = list(pageLength = 15, scrollX = TRUE, dom = 'Brtp',
                             buttons = list(
                               list(extend = 'csv', text = 'Export CSV', filename = 'Variances'),
                               list(extend = 'excel', text = 'Export Excel', filename = 'Variances')
                             )))
  }, server = FALSE)
  
  # Variance warning
  output$variance_warning <- renderUI({
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    
    fit <- cfa_fit_list()[[cur_id]]$fit
    sol <- tryCatch(lavaan::parameterEstimates(fit), error = function(e) NULL)
    
    if (is.null(sol)) return(NULL)
    
    variances <- sol[sol$op == "~~" & sol$lhs == sol$rhs, ]
    has_negative <- any(variances$est < 0, na.rm = TRUE)
    
    if (has_negative) {
      tags$div(
        class = "alert alert-danger",
        icon("exclamation-triangle"),
        tags$b(" Warning: Heywood case detected! One or more variances are negative. This may indicate a problem with the model or data.")
      )
    } else {
      NULL
    }
  })

  # HTML Report Download Handler
  # Modal for HTML export
  observeEvent(input$btn_export_modal, {
    showModal(modalDialog(
      title = "Export HTML Report",
      tags$div(
        style = "text-align: center;",
        tags$h4("Your report is ready to download!"),
        tags$p("If you find projectLSA helpful for your research, consider supporting its continuous development. Your support helps us keep the project alive!"),
        tags$div(
          style = "background-color: #f8f9fa; border: 2px dashed #17a2b8; padding: 20px; border-radius: 10px; margin-bottom: 20px;",
          tags$strong("Support via Bank BNI:", style="color: #17a2b8; font-size: 16px;"), tags$br(),
          tags$span("0331355050", style="font-size: 24px; font-weight: bold; color: #d32f2f; letter-spacing: 2px;"), tags$br(),
          tags$em("Account Name: Hasan Djidu"), tags$br(), tags$br(),
          tags$strong("International Transfer (SWIFT):", style="color: #17a2b8; font-size: 14px;"), tags$br(),
          tags$span("BNINIDJA", style="font-size: 16px; font-weight: bold; color: #d32f2f;")
        ),
        downloadButton("download_report_html", "Download Report Now", class = "btn btn-success btn-lg")
      ),
      footer = modalButton("Close"),
      easyClose = TRUE
    ))
  })
  
  output$download_report_html <- downloadHandler(
    filename = function() {
      cur_id <- cfa_current_id()
      if(is.null(cur_id)) return(paste0("Report_", Sys.Date(), ".html"))
      
      fit_obj <- cfa_fit_list()[[cur_id]]$fit
      pe <- lavaan::parameterEstimates(fit_obj)
      
      # Detect if SEM
      is_sem <- any(pe$op == "~")
      prefix <- if (is_sem) "SEM_Report_" else "CFA_Report_"
      
      # Extract main latent name
      latents <- unique(pe$lhs[pe$op == "=~"])
      latent_name <- if(length(latents) > 0) latents[length(latents)] else "Model"
      
      paste0(prefix, latent_name, "_", Sys.Date(), ".html")
    },
    content = function(file) {
      req(cfa_current_id())
      cur_id <- cfa_current_id()
      fit_info <- cfa_fit_list()[[cur_id]]
      
      report_path <- file.path(system.file("app", package = "projectLSA"), "cfa_report.Rmd")
      if (report_path == "" || !file.exists(report_path)) {
        report_path <- "cfa_report.Rmd"
      }
      
      tempReport <- file.path(tempdir(), "cfa_report.Rmd")
      file.copy(report_path, tempReport, overwrite = TRUE)
      
      # Get plot colors safely for the params
      if (input$plot_color_scheme == "Custom") {
        man_col <- input$mancolour
        lat_col <- input$latcolour
      } else {
        colors <- get_color_scheme(input$plot_color_scheme)
        man_col <- colors$man
        lat_col <- colors$lat
      }
      
      # Get all models for history
      lst <- cfa_fit_list()
      fit_initial <- lst[[1]]$fit
      
      model_history_df <- do.call(rbind, lapply(lst, function(x){
        m <- x$measures
        data.frame(
          Model = x$id,
          Note = ifelse(is.null(x$note) || x$note == "", "-", x$note),
          chisq = m["chisq"],
          df = m["df"],
          pvalue = m["pvalue"],
          RMSEA = m["rmsea"],
          CFI = m["cfi"],
          TLI = m["tli"],
          SRMR = m["srmr"],
          stringsAsFactors = FALSE
        )
      }))
      
      # Render to a temporary html file first to ensure pandoc handles extensions correctly
      out_html <- file.path(tempdir(), "cfa_report_out.html")
      
      # Detect model type
      pe <- lavaan::parameterEstimates(fit_info$fit)
      model_type <- if(any(pe$op == "~")) "SEM" else "CFA"
      
      showModal(modalDialog("Generating Report...", footer = NULL))
      tryCatch({
        rmarkdown::render(tempReport, output_file = out_html,
          params = list(
            model_type = model_type,
            fit_final = fit_info$fit,
            fit_initial = fit_initial,
            model_history_df = model_history_df,
            dec = input$cfa_dec_sep,
            ave = fit_info$ave,
            relsem = fit_info$relsem,
            htmt = fit_info$htmt,
            plot_standardized = input$plot_standardized,
            plot_style = input$plot_style,
            plot_layout = input$plot_layout,
            plot_residuals = input$plot_residuals,
            plot_exoCov = input$plot_exoCov,
            plot_edge_label_size = input$plot_edge_label_size,
            plot_nodesize_lat = input$plot_nodesize_lat,
            plot_nodesize_man = input$plot_nodesize_man,
            plot_nodesize_man2 = input$plot_nodesize_man2,
            plot_rotation = input$plot_rotation,
            man_col = man_col,
            lat_col = lat_col,
            bifactor = input$bifactor,
            edgewidth = input$edgewidth,
            plotwidth = input$plotwidth,
            plotheight = input$plotheight
          ),
          envir = new.env(parent = globalenv())
        )
        
        # Copy to the Shiny-provided 'file' path
        file.copy(out_html, file, overwrite = TRUE)
        
      }, error = function(e) {
        showNotification(paste("Error generating report:", e$message), type = "error")
      }, finally = {
        removeModal()
      })
    },
    contentType = "text/html"
  )
  # Modification indices
  mi_df <- reactive({
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    
    fit <- cfa_fit_list()[[cur_id]]$fit
    mi <- tryCatch(lavaan::modindices(fit, sort. = TRUE, minimum.value = 1), error = function(e) NULL)
    
    if (is.null(mi) || nrow(mi) == 0) return(NULL)
    
    if (nrow(mi) > 20) mi <- mi[1:20, ]
    
    mi
  })
  
  output$mi_table <- renderDT({
    mi <- mi_df()
    #mi <- mi %>% dplyr::mutate(across(where(is.numeric), ~ round(.x, 3)))
    if (is.null(mi)) return(datatable(data.frame(Message = "No modification indices available")))
    
    mi$Action <- sapply(1:nrow(mi), function(i) {
      as.character(
        actionButton(
          class = "btn-primary sm",
          style = "font-size: 11px !important; padding: 2px 10px !important;",  
          inputId = paste0("mi_btn_", i),
          label = "Use This",
          onclick = paste0('Shiny.setInputValue("mi_selected", "', i, '", {priority: "event"})')
        )
      )
    })
    
    display_mi <- mi[, c("lhs", "op", "rhs", "mi", "epc", "Action")]
    colnames(display_mi) <- c("From", "Operator", "To", "MI", "EPC", "Action")
    
    datatable(display_mi, rownames = FALSE, escape = FALSE, 
              options = list(pageLength = 10, scrollX = TRUE,dom = 'rtp',
                             # Atur ukuran font dan styling
                             initComplete = JS(
                               "function(settings, json) {",
                               "$(this.api().table().header()).css({'font-size': '5px', 'font-weight': 'bold'});",
                               "$(this.api().table().body()).css({'font-size': '5px', 'line-height': '1'});",
                               "}"
                             )))    %>% 
      formatRound(columns = which(sapply(display_mi, is.numeric)), digits = 3)
  }, server = FALSE)
  
  # Handle MI button clicks
  observeEvent(input$mi_selected, {
    req(input$mi_selected)
    
    mi <- mi_df()
    if (is.null(mi)) return()
    
    idx <- as.numeric(input$mi_selected)
    if (idx > nrow(mi)) return()
    
    row <- mi[idx, ]
    
    if (row$op == "=~") {
      new_line <- paste(row$lhs, "=~", row$rhs)
    } else if (row$op == "~~") {
      new_line <- paste(row$lhs, "~~", row$rhs)
    } else if (row$op == "~") {
      new_line <- paste(row$lhs, "~", row$rhs)
    } else {
      new_line <- paste(row$lhs, row$op, row$rhs)
    }
    
    current_model <- input$cfa_model_text
    updated_model <- paste(current_model, "\n", new_line)
    
    updateTextAreaInput(session, "cfa_model_text", value = updated_model)
    
    showNotification(paste("Added MI suggestion to model:", new_line), type = "message")
  })
  
  # Update fungsi get_color_scheme
  get_color_scheme <- function(scheme_name, custom_man = NULL, custom_lat = NULL) {
    if (scheme_name == "Custom" && !is.null(custom_man) && !is.null(custom_lat)) {
      return(list(man = custom_man, lat = custom_lat))
    }
    
    switch(scheme_name,
           "Blue-Yellow" = list(
             man = "#A1E3F9",
             lat = c("#FFFFBA")
           ),
           "Ocean" = list(
             man = "#B3E5FC",
             lat = c("#81D4FA", "#4FC3F7", "#29B6F6")
           ),
           "Forest" = list(
             man = "#C8E6C9",
             lat = c("#A5D6A7", "#81C784", "#4CAF50")
           ),
           "Rainbow" = list(
             man = "#B3E5FC", 
             lat = c("#FF8A65", "#81D4FA", "#AED581", "#CE93D8","#FFF176" )
           ),
           "Pastel" = list(
             man = "#F8ECE5",
             lat = c("#E2E2E2", "#E8EAF6", "#F3E5F5", "#E0F2F1")
           ),
           "Greyscale" = list(
             man = "#EEEEEE",
             lat = c("#DDDDDD", "#CCCCCC", "#BBBBBB", "#AAAAAA")
           ),
           "Earth" = list(
             man = "#D7CCC8",
             lat = c("#BCAAA4", "#A1887F", "#8D6E63", "#795548")
           ),
           "Vibrant" = list(
             man = "#FFE0B2",
             lat = c("#FF5252", "#E040FB", "#7C4DFF", "#536DFE")
           ),
           "Monochrome" = list(
             man = "#E0E0E0",
             lat = c("#9E9E9E", "#757575", "#616161", "#424242")
           ),
           "Sunset" = list(
             man = "#FFCCBC",
             lat = c("#FF8A65", "#FF7043", "#F4511E", "#E64A19")
           ),
           "Rose" = list(
             man = "#F8BBD0",
             lat = c("#F06292", "#EC407A", "#E91E63", "#D81B60")
           ),
           "Mint" = list(
             man = "#B2DFDB",
             lat = c("#4DB6AC", "#26A69A", "#009688", "#00897B")
           )
    )
  }
  
  
  # Get factor names safely
  get_factor_names <- function(fit_obj) {
    tryCatch({
      if (inherits(fit_obj, "lavaan")) {
        params <- lavaan::parameterEstimates(fit_obj)
        factors <- unique(params$lhs[params$op == "=~"])
        if (length(factors) > 0) return(factors)
        
        model_text <- fit_obj@Model@model.syntax
        if (is.character(model_text)) {
          lines <- strsplit(model_text, "\n")[[1]]
          factor_lines <- grep("=~", lines, value = TRUE)
          factors <- sapply(strsplit(factor_lines, "=~"), function(x) trimws(x[1]))
          return(factors)
        }
      }
      return("Unknown")
    }, error = function(e) {
      return("Error getting factors")
    })
  }
  
  # Helper to generate the fit text
  get_fit_text <- function(m, selected, dec_sep = ".") {
    all_indices <- c("chisq","df","pvalue","rmsea","cfi","gfi","srmr","tli","nfi")
    if ("SELECT ALL" %in% selected) selected <- all_indices
    selected <- intersect(all_indices, selected)
    labels <- c(chisq="χ²", df="df", pvalue="p", rmsea="RMSEA", cfi="CFI", gfi="GFI", srmr="SRMR", tli="TLI", nfi="NFI")
    format_val <- function(name, val) {
      if (is.na(val)) return("NA")
      fmt_with_sep <- function(x) {
        if(dec_sep == ",") gsub("\\.", ",", x) else x
      }
      if (name == "pvalue") return(fmt_with_sep(format.pval(val, digits = 3)))
      if (name == "chisq")  return(fmt_with_sep(as.character(round(val, 2))))
      if (name == "df")     return(val)
      return(fmt_with_sep(sprintf("%.3f", val)))
    }
    paste(sapply(selected, function(n) paste0(labels[n], "=", format_val(n, m[n]))), collapse = "; ")
  }

  render_cfa_plot_impl <- function() {
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) {
      plot(1, 1, type = "n", xlab = "", ylab = "", axes = FALSE, main = "No CFA model available")
      text(1, 1, "Please run a CFA model first\n\n1. Select 'Built-in: HolzingerSwineford1939'\n2. Keep the example model\n3. Click 'Run CFA'", cex = 1.2, col = "darkred")
      return(invisible(NULL))
    }
    
    obj <- cfa_fit_list()[[cur_id]]
    fit_obj <- obj$fit
    m <- obj$measures
    
    if (input$plot_color_scheme == "Custom") {
      colors <- list(man = input$mancolour, lat = input$latcolour)
    } else {
      colors <- get_color_scheme(input$plot_color_scheme)
    }
    
    # Fix bottom margin so plot expands upwards
    b_margin <- 6
    A <- semPlot::semPaths(
      fit_obj,
      what = ifelse(input$plot_standardized, "std", "est"),
      style = input$plot_style,
      layout = input$plot_layout,
      residuals = input$plot_residuals,
      exoCov = input$plot_exoCov,
      edge.label.cex = input$plot_edge_label_size,
      nCharNodes = 6,
      nCharEdges = 6,
      sizeLat = input$plot_nodesize_lat,
      sizeMan = input$plot_nodesize_man,
      sizeMan2 = input$plot_nodesize_man2,
      rotation = input$plot_rotation,
      color = colors,
      label.color = "black",
      mar = c(b_margin, 15-input$plotwidth, 20-input$plotheight, 15-input$plotwidth),
      bifactor = input$bifactor,
      esize = input$edgewidth,
      edge.color = "black",       
      freeStyle = c(1, "black"),
      fixedStyle = c(2, "black"),
      fade = FALSE,
      DoNotPlot = TRUE
    )
    
    # Add significance
    A <- semptools::mark_sig(semPaths_plot = A, object = fit_obj,
                             alphas = c("*" = 0.05, "**" = 0.01, "***" = 0.001))
    
    # Apply decimal separator to edge labels
    if (input$cfa_dec_sep == ",") {
      A$graphAttributes$Edges$labels <- gsub("\\.", ",", A$graphAttributes$Edges$labels)
    }
    
    # Plot the graph
    plot(A)
    
    # Add fit info inside a box
    factor_names <- get_factor_names(fit_obj)
    fit_text_raw <- get_fit_text(m, input$fit_indices_selected, dec_sep = input$cfa_dec_sep)
    wrapped_fit <- paste(strwrap(paste0("Fit indices: ", fit_text_raw), width = 110), collapse = "\n")
    
    estimator_text <- paste0("Estimator = ", input$cfa_estimator, " | Factors: ", paste(factor_names, collapse = ", "))
    sig_sign <- '*** : p ≤ 0.001", "** : p ≤ 0.01", "* : p ≤ 0.05'
    
    legend_text <- c(wrapped_fit, estimator_text, sig_sign)
    text_colors <- c("#2c3e50", "darkblue", "#6c757d")
    
    # Get legend coordinates without plotting
    leg_info <- legend("bottom", legend = legend_text, plot = FALSE, inset = c(0, 0), cex = 0.9, y.intersp = 1.0)
    
    # Draw empty legend box with correct width
    max_w <- max(strwidth(legend_text, cex = 0.9))
    legend("bottom", legend = rep("", length(legend_text)), bg = "#f8f9fa", box.col = "#e2e8f0", 
           inset = c(0, 0), cex = 0.9, xpd = TRUE, y.intersp = 1.0, text.width = max_w)
           
    # Draw text centered
    center_x <- leg_info$rect$left + leg_info$rect$w / 2
    y_coords <- leg_info$text$y
    for(i in seq_along(legend_text)) {
      text(x = center_x, y = y_coords[i], labels = legend_text[i], col = text_colors[i], cex = 0.9, xpd = TRUE, adj = c(0.5, 0.5))
    }
    # (Watermark has been removed as requested)
  }

  output$path_plot <- renderPlot({
    tryCatch({
      render_cfa_plot_impl()
    }, error = function(e) {
      plot(1, 1, type = "n", xlab = "", ylab = "", axes = FALSE, main = "Plot Error")
      text(1, 1, paste("Error creating plot:\n", e$message), cex = 1.2, col = "red")
    })
  })
  
  output$download_cfa_plot <- downloadHandler(
    filename = function() {
      paste0("CFA_Plot_", Sys.Date(), ".png")
    },
    content = function(file) {
      png(file, width = 12, height = 10, units = "in", res = 300)
      tryCatch({
        render_cfa_plot_impl()
      }, finally = {
        dev.off()
      })
    }
  )
  
  observeEvent(input$fit_indices_selected, {
    sel <- input$fit_indices_selected
    if ("SELECT ALL" %in% sel && length(sel) > 1)
      updateSelectInput(session, "fit_indices_selected",
                        selected = if (tail(sel,1)=="SELECT ALL") "SELECT ALL"
                        else setdiff(sel,"SELECT ALL"))
  })
  
  # Fit information below plot
  output$plot_fit_info <- renderUI({
    cur_id <- cfa_current_id()
    if (is.null(cur_id)) return(NULL)
    
    obj <- cfa_fit_list()[[cur_id]]
    m <- obj$measures
    fit_obj <- obj$fit
    
    # Get factor names safely
    factor_names <- get_factor_names(fit_obj)
    
    # fit_text <- paste0(
    #   "χ² = ", round(m["chisq"], 2), "; ",
    #   "df = ", m["df"], "; ",
    #   "p = ", ifelse(is.na(m["pvalue"]), "NA", format.pval(m["pvalue"], digits = 3)), "; ",
    #   "RMSEA = ", ifelse(is.na(m["rmsea"]), "NA", sprintf("%.3f", m["rmsea"])), "; ",
    #   "CFI = ", ifelse(is.na(m["cfi"]), "NA", sprintf("%.3f", m["cfi"])), "; ",
    #   "GFI = ", ifelse(is.na(m["gfi"]), "NA", sprintf("%.3f", m["gfi"])), "; ",
    #   "SRMR = ", ifelse(is.na(m["srmr"]), "NA", sprintf("%.3f", m["srmr"])),"; ",
    #   "TLI = ", ifelse(is.na(m["tli"]), "NA", sprintf("%.3f", m["tli"])),"; ",
    #   "NFI = ", ifelse(is.na(m["nfi"]), "NA", sprintf("%.3f", m["nfi"]))
    # 
    # )
    selected <- input$fit_indices_selected
    fit_text <- get_fit_text(m, selected, dec_sep = input$cfa_dec_sep)
    
    
    
    
    estimator_text <- paste0(
      "Estimator = ", input$cfa_estimator, 
      " | Factors: ", paste(factor_names, collapse = ", ")
    )
    sig_sign <- paste0(
      '*** : p ≤ 0.001", "** : p ≤ 0.01", "* : p ≤ 0.05'
    )
    
    tagList(
      tags$div(style = "margin-top: 0px; padding: 5px; background-color: #f8f9fa; border-radius: 5px; text-align: center;",
               tags$p(style = "margin: 0; font-size: 11px; color: #2c3e50;", 
                      tags$b("Fit indices: "), fit_text),
               tags$p(style = "margin: 0; font-size: 11px; color: darkblue;", 
                      estimator_text),
               tags$p(style = "margin: 0; font-size: 10px; color: #6c757d;", 
                      sig_sign)
      )
    )
  })
  
  # Download handlers
  output$download_ris <- downloadHandler(
    filename = function() {
      "projectLSA_citation.ris"
    },
    content = function(file) {
      ris_text <- c(
        "TY  - JOUR",
        "AU  - Djidu, Hasan",
        "AU  - Retnawati, Heri",
        "AU  - Hadi, Samsul",
        "AU  - Haryanto",
        "PY  - 2026",
        "DA  - 2026/04/20",
        "TI  - projectLSA: A Shiny Application for Integrated Latent Structure Analysis",
        "JO  - Applied Psychological Measurement",
        "DO  - 10.1177/01466216261446305",
        "UR  - https://doi.org/10.1177/01466216261446305",
        "ER  - "
      )
      writeLines(ris_text, file)
    }
  )

  
  
}
