inst/shiny-examples/fcaRviz/server.R

# ==============================================================================
# server.R - VERSIÓN 3.2 (FINAL: CONVERSATIONAL SUB-LATTICE & HIERARCHY)
# ==============================================================================

source("uiHome.R")
source("uiUploadData.R")
source("uiBasicOperations.R")
source("uiImplications.R")
source("uiConcepts.R")

server <- function(input, output, session) {

  # ===========================================================================
  # 1. CONFIGURACIÓN GENERAL Y NAVEGACIÓN
  # ===========================================================================
  observeEvent(input$main_nav, { nav_select("hidden_tabs", selected = input$main_nav) })

  observeEvent(input$theme_selector, {
    req(input$theme_selector)
    theme_name <- tolower(input$theme_selector)
    new_theme <- bs_theme_update(my_theme, bootswatch = theme_name)
    session$setCurrentTheme(new_theme)
    
    if (theme_name %in% c("darkly", "slate", "cyborg", "superhero")) {
      shinyjs::addClass(selector = "body", class = "dark-mode")
    } else {
      shinyjs::removeClass(selector = "body", class = "dark-mode")
    }
  })

  output$dataHeaderTitle <- renderText({ if(input$editMode) "Data Editor" else "Data Viewer" })

  # --- VARIABLES REACTIVAS ---
  vals <- reactiveValues(
    # Core Data
    fc = NULL,
    trigger = 0,
    stats_orig = NULL,
    filtered_imps = NULL,

    # Association rules (Concepts Lattice tab)
    assoc_rules_df = NULL,

    force_interactive_render = FALSE,
    concept_filter_ids = NULL,
    tab_concept_filter_ids = NULL,

    # Attribute Exploration State (ConExp)
    exploration_active = FALSE,
    exploration_complete = FALSE,
    confirmed_rules = character(0),
    refuted_rules = character(0),
    current_question = NULL,

    # Multivalued & Scaling State
    multivalued_df = NULL,
    current_doc = NULL,

    # Object Exploration State (Transposed ConExp)
    obj_exploration_active = FALSE,
    obj_exploration_complete = FALSE,
    obj_confirmed_rules = character(0),
    obj_refuted_rules = character(0),
    obj_current_question = NULL,
    fc_transposed = NULL,
    
    # Selected concept focal
    active_concept_id = 1,

    # Context table: arrow relations overlay (fcaR 1.7+)
    show_arrows = FALSE,

    # Lattice explorer overlay panel
    lattice_info_visible = FALSE,
    lattice_info_pinned = FALSE,
    lattice_info_title = NULL,
    lattice_info_body = NULL,
    lattice_info_kind = "concept",
    lattice_info_nonce = 0,
    lattice_edges_cache = NULL,
    active_edge_from = NULL,
    active_edge_to = NULL,
    active_edge_nonce = 0,
    
    # Inline refutation tracking for Ganter Attribute Exploration
    is_refuting = FALSE
  )

  # Background calculation state
  bg_calc <- reactiveValues(
    process = NULL,
    start_time = NULL,
    task_type = NULL # "concepts" or "implications"
  )

  # Polling observer for background processes
  observe({
    req(bg_calc$process)
    invalidateLater(1000) # Poll every 1 second
    
    # Calculate elapsed time
    elapsed <- round(as.numeric(difftime(Sys.time(), bg_calc$start_time, units = "secs")), 1)
    
    # Update notification with elapsed time
    showNotification(
      id = "bg_calc_notification",
      paste0("Computing ", bg_calc$task_type, "... Elapsed time: ", elapsed, " seconds. Please wait, R session remains responsive."),
      duration = NULL,
      closeButton = FALSE,
      type = "warning"
    )
    
    if (!bg_calc$process$is_alive()) {
      p <- bg_calc$process
      type <- bg_calc$task_type
      bg_calc$process <- NULL
      removeNotification("bg_calc_notification")
      
      status <- p$get_exit_status()
      if (!is.null(status) && status != 0) {
        shinyalert("Error", "The background calculation failed or ran out of memory. Try reducing context size.", type = "error")
        return()
      }
      
      tryCatch({
        res <- p$get_result()
        vals$fc <- res
        vals$trigger <- vals$trigger + 1
        
        if (type == "concepts") {
          shinyalert("Done!", "Concepts computed successfully.", type = "success")
        } else {
          # Update implications statistics & cached objects
          vals$stats_orig <- list(
            count = vals$fc$implications$cardinality(),
            size_lhs = sum(vals$fc$implications$get_LHS_matrix()),
            size_rhs = sum(vals$fc$implications$get_RHS_matrix())
          )
          vals$filtered_imps <- vals$fc$implications
          implications_df(get_implications_dataframe(vals$fc$implications))
          shinyalert("Done!", "Implications computed successfully.", type = "success")
        }
      }, error = function(e) {
        shinyalert("Error", paste("Failed to retrieve calculation result:", e$message), type = "error")
      })
    }
  })

  implications_df <- reactiveVal(NULL)

  sync_implications_ui <- function() {
    vals$stats_orig <- list(
      count = vals$fc$implications$cardinality(),
      size_lhs = sum(vals$fc$implications$get_LHS_matrix()),
      size_rhs = sum(vals$fc$implications$get_RHS_matrix())
    )
    vals$filtered_imps <- vals$fc$implications
    implications_df(get_implications_dataframe(vals$fc$implications))
    vals$trigger <- vals$trigger + 1
  }

  run_canonical_basis <- function(from_context = FALSE) {
    if (from_context) {
      safe_find_implications(vals$fc)
    } else {
      has_imps <- tryCatch(vals$fc$implications$cardinality() > 0, error = function(e) FALSE)
      if (has_imps) {
        vals$fc$implications <- vals$fc$implications$to_basis()
      } else {
        safe_find_implications(vals$fc)
      }
    }
    sync_implications_ui()
  }

  reset_context_derived_state <- function() {
    vals$stats_orig <- NULL
    vals$filtered_imps <- NULL
    implications_df(NULL)
  }

  # Reset force_interactive_render flag on any dataset/context change
  observeEvent(vals$trigger, {
    vals$force_interactive_render <- FALSE
  })

  # --- CARGA INICIAL (DESACTIVADA) ---
  # observe({
  #   if(is.null(vals$fc)){
  #     try({
  #       data("planets", package = "fcaR", envir = environment())
  #       if(exists("planets")){
  #         vals$fc <- FormalContext$new(planets)
  #         vals$trigger <- vals$trigger + 1
  #         shinyalert("Welcome to fcaRviz", "Default dataset loaded.", type = "info", closeOnEsc = TRUE)
  #       }
  #     }, silent = TRUE)
  #   }
  # })

  # --- ACTUALIZADOR DE INPUTS (DROPDOWNS) ---
  observe({
    vals$trigger; req(vals$fc)
    objs <- if(!is.null(vals$fc$objects)) vals$fc$objects else character(0)
    atts <- if(!is.null(vals$fc$attributes)) vals$fc$attributes else character(0)

    # Basic & Concepts Inputs
    updateSelectInput(session, "intentOptions", choices = objs)
    updateSelectInput(session, "extentOptions", choices = atts)
    updateSelectInput(session, "closureOptions", choices = atts)

    # Implications Inputs
    updateSelectInput(session, "selectLHS", choices = atts)
    updateSelectInput(session, "selectRHS", choices = atts)
    updateSelectInput(session, "selectNotLHS", choices = atts)
    updateSelectInput(session, "selectNotRHS", choices = atts)
    updateSelectInput(session, "selectClosure", choices = atts)
    updateSelectInput(session, "hypLHS", choices = atts)
    updateSelectInput(session, "hypRHS", choices = atts)

    # Concept Filters Inputs
    updateSelectizeInput(session, "filterObjs", choices = objs, server = TRUE)
    updateSelectizeInput(session, "filterAtts", choices = atts, server = TRUE)
    updateSelectizeInput(session, "filterObjsTab", choices = objs, server = TRUE)
    updateSelectizeInput(session, "filterAttsTab", choices = atts, server = TRUE)
  })

  # --- BROADCAST CONTEXT STATE TO JS (used by all conditionalPanels) ---
  observe({
    if (is.null(vals$fc)) {
      shinyjs::runjs("Shiny.setInputValue('contextLoaded', false, {priority: 'event'});")
    } else {
      shinyjs::runjs("Shiny.setInputValue('contextLoaded', true, {priority: 'event'});")
    }
  })

  # --- SHOW/HIDE CONTEXT TABS AND PLACEHOLDER ---

  output$noContextPlaceholderUI <- renderUI({
    if (is.null(vals$fc)) {
      card(
        class = "border-0 shadow-sm py-5 text-center text-muted",
        div(class = "py-5",
            icon("table", class = "fa-5x mb-4 text-muted"),
            h3("No Formal Context Loaded", class = "fw-bold text-dark"),
            p(class = "text-muted fs-5 px-4",
              "Choose an option from the 'Context origin' menu at the top right to import, connect, or generate a context.")
        )
      )
    }
  })

  # Single observer: show/hide all context-gated divs across all tabs
  observe({
    if (is.null(vals$fc)) {
      shinyjs::show("impl_no_ctx");   shinyjs::hide("impl_content")
      shinyjs::show("basic_no_ctx");  shinyjs::hide("basic_content")
      shinyjs::show("concepts_no_ctx"); shinyjs::hide("concepts_content")
      shinyjs::hide("contextTabsWrapper")
    } else {
      shinyjs::hide("impl_no_ctx");   shinyjs::show("impl_content")
      shinyjs::hide("basic_no_ctx");  shinyjs::show("basic_content")
      shinyjs::hide("concepts_no_ctx"); shinyjs::show("concepts_content")
      shinyjs::show("contextTabsWrapper")
    }
  })

  # Show/hide edit vs view panels inside the context tabs
  observe({
    req(vals$fc)
    if (isTRUE(input$editMode)) {
      shinyjs::hide("viewModePanel")
      shinyjs::show("editModePanel")
    } else {
      shinyjs::show("viewModePanel")
      shinyjs::hide("editModePanel")
    }
  })

  # Hide Multivalued Scaling tab unless multivalued data is present
  observe({
    req(vals$fc)
    shinyjs::delay(300, {
      if (is.null(vals$multivalued_df)) {
        bslib::nav_hide(id = "data_management_tabs", target = "Multivalued Scaling")
      } else {
        bslib::nav_show(id = "data_management_tabs", target = "Multivalued Scaling", select = FALSE)
      }
    })
  })



  # ===========================================================================
  # 2. GESTIÓN DE DATOS (UPLOAD, SAVE, EDITOR)
  # ===========================================================================
  
  # --- CONTEXT ORIGIN DROPDOWN LINKS ---
  observeEvent(input$optImportFile, {
    showModal(modalDialog(
      title = span(icon("file-arrow-up", class="text-primary"), " Import Formal Context or Load Project"),
      size = "m",
      easyClose = TRUE,
      footer = modalButton("Close"),
      fileInput("file", "Select file (.csv, .rds, .cxt, .cex):", accept = c(".csv", ".rds", ".cxt", ".cex"),
                buttonLabel = "Load...", placeholder = "No file selected", width="100%"),
      helpText("Upload a full state (.rds), binary/multivalued table (.csv), CXT context (.cxt), or ConExp (.cex).")
    ))
  })

  observeEvent(input$optConnectRepo, {
    showModal(modalDialog(
      title = span(icon("github", class="text-primary"), " Connect with Repository"),
      size = "m",
      easyClose = TRUE,
      footer = modalButton("Close"),
      selectInput("selectDataset", "Select Example Dataset from Repo:", choices = c("Loading available datasets..." = ""), width = "100%"),
      helpText("Datasets are being fetched from the remote repository. Please wait...")
    ))
    
    # Automatically initiate connection
    tryCatch({
      opts <- selectOptions()
      if (length(opts[[1]]) > 0 && opts[[1]][1] != "Error. Connection failed.") {
        # Format choices list with names and placeholder to prevent automatic load
        choices_list <- setNames(opts[[1]], opts[[2]])
        choices_list <- c("Select a dataset to load..." = "", choices_list)
        updateSelectInput(session, "selectDataset", choices = choices_list)
      } else {
        updateSelectInput(session, "selectDataset", choices = c("Error: Connection failed. Check internet connection." = ""))
      }
    }, error = function(e) {
      updateSelectInput(session, "selectDataset", choices = c("Error: Connection failed" = ""))
    })
  })

  observeEvent(input$optGenerateSynthetic, {
    showModal(modalDialog(
      title = div(class="d-flex justify-content-between align-items-center w-100",
                  span(icon("dice", class="text-primary"), " Synthetic Data Generators"),
                  actionLink("btnSyntheticInfo", "", icon = icon("info-circle"), style = "cursor: pointer; color: #0d6efd; font-size: 1.2em;", title = "Show synthetic generators info & references")
      ),
      size = "m",
      easyClose = TRUE,
      footer = modalButton("Close"),
      
      selectInput("rand_type", "Generator / Perturbator:", 
                  choices = c("Random Context (Uniform/Dirichlet)" = "random_context",
                              "Random Distributive Context" = "random_distributive")),
      
      conditionalPanel(
        condition = "input.rand_type == 'random_context'",
        numericInput("rand_objs", "Number of Objects:", value = 10, min = 1, step = 1),
        numericInput("rand_atts", "Number of Attributes:", value = 8, min = 1, step = 1),
        sliderInput("rand_density", "Density:", min = 0.05, max = 0.95, value = 0.2, step = 0.05),
        selectInput("rand_distribution", "Distribution Method:", choices = c("uniform", "dirichlet")),
        conditionalPanel(
          condition = "input.rand_distribution == 'dirichlet'",
          numericInput("rand_alpha", "Alpha parameter:", value = 1.0, min = 0.1, step = 0.1)
        )
      ),
      
      conditionalPanel(
        condition = "input.rand_type == 'random_distributive'",
        numericInput("dist_elements", "Number of Elements:", value = 10, min = 2, step = 1),
        sliderInput("dist_density", "Density:", min = 0.05, max = 0.95, value = 0.2, step = 0.05)
      ),
      
      hr(),
      actionButton("btnGenerateSynthetic", "Generate & Load Context", class = "btn-info w-100 text-white fw-bold", icon = icon("dice"))
    ))
  })

  observeEvent(input$optCreateEmpty, {
    m <- matrix(0, nrow=3, ncol=3); rownames(m) <- c("Obj1","Obj2","Obj3"); colnames(m) <- c("Att1","Att2","Att3")
    vals$fc <- FormalContext$new(m); vals$trigger <- vals$trigger + 1; implications_df(NULL)
    vals$stats_orig <- NULL
    
    vals$current_doc <- list(
      title = "New Empty Context Template",
      description = "A blank 3x3 template initialized for manual grid editing.",
      source = "Manual Creation (fcaRviz UI Template)",
      creation_method = "Empty Template",
      timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
      history_log = list(paste0("Created empty 3x3 context at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")))
    )
    
    updateMaterialSwitch(session, "editMode", value = TRUE)
    shinyalert("New Context", "Created empty context and enabled Edit Mode.", type = "success")
  })

  observeEvent(input$btnRandomizeLoaded, {
    req(vals$fc)
    showModal(modalDialog(
      title = span(icon("dice", class="text-primary"), " Randomize Current Context"),
      size = "m",
      easyClose = TRUE,
      footer = modalButton("Close"),
      selectInput("pert_method", "Randomization Method:", choices = c("swap", "rewire")),
      numericInput("pert_iterations", "Iterations (NULL for auto):", value = NA, min = 1, step = 1),
      hr(),
      actionButton("btnPerturbCurrent", "Apply Randomization", class = "btn-warning w-100 text-dark fw-bold", icon = icon("sync"))
    ))
  })

  observeEvent(input$file, {
    vals$current_doc <- NULL
    req(input$file); ext <- tolower(tools::file_ext(input$file$name))
    withProgress(message = 'Loading...', value = 0.5, {
      tryCatch({
        if (ext == "rds") {
          loaded_obj <- readRDS(input$file$datapath)
          vals$multivalued_df <- NULL
          if(inherits(loaded_obj, "FormalContext")) {
            vals$fc <- loaded_obj
            if(!is.null(vals$fc$implications) && vals$fc$implications$cardinality() > 0)
              implications_df(get_implications_dataframe(vals$fc$implications))
            removeModal()
            shinyalert("Project Loaded", "Context restored.", type = "success")
          } else { 
            vals$fc <- FormalContext$new(loaded_obj)
            removeModal()
            shinyalert("Data Loaded", "Matrix loaded.", type = "success") 
          }
        } else if (ext == "cxt") {
          clean_path <- file.path(tempdir(), paste0("upload_", as.numeric(Sys.time()), ".cxt"))
          lines <- readLines(input$file$datapath, warn = FALSE, encoding = "UTF-8")
          writeLines(lines, clean_path, useBytes = TRUE)
          vals$fc <- FormalContext$new(clean_path)
          vals$multivalued_df <- NULL
          removeModal()
          shinyalert("Success", "CXT loaded.", type = "success")
        } else if (ext == "cex") {
          mat <- parse_cex(input$file$datapath)
          vals$fc <- FormalContext$new(mat)
          vals$multivalued_df <- NULL
          removeModal()
          shinyalert("Success", "ConExp .cex file loaded successfully.", type = "success")
        } else {
          # Load and detect CSV column structures
          raw_df <- NULL
          try({
            raw_df <- read.csv(input$file$datapath, row.names = 1, stringsAsFactors = FALSE, check.names = FALSE)
          }, silent = TRUE)
          if (is.null(raw_df) || ncol(raw_df) == 0) {
            try({
              raw_df <- read.csv(input$file$datapath, stringsAsFactors = FALSE, check.names = FALSE)
            }, silent = TRUE)
          }
          
          if (!is.null(raw_df) && ncol(raw_df) > 0) {
            is_bin <- all(sapply(raw_df, is_binary_vector))
            if (is_bin) {
              vals$fc <- FormalContext$new(input$file$datapath)
              vals$multivalued_df <- NULL
              removeModal()
              shinyalert("Success", "Binary CSV context loaded successfully.", type = "success")
            } else {
              vals$multivalued_df <- raw_df
              
              # Pre-render a safe nominal default context to prevent application crashes
              default_binary <- perform_conceptual_scaling(raw_df)
              vals$fc <- FormalContext$new(as.matrix(default_binary))
              
              removeModal()
              shinyalert("Multivalued Data Loaded", 
                         "This CSV contains non-binary numerical or categorical columns. Configure your scaling options in the 'Multivalued Scaling' tab to binarize it.", 
                         type = "info")
              
            }
          } else {
            vals$fc <- FormalContext$new(input$file$datapath)
            vals$multivalued_df <- NULL
            removeModal()
            shinyalert("Success", "CSV context loaded.", type = "success")
          }
        }

        # Set current_doc metadata for imported files
        file_desc <- if (!is.null(vals$multivalued_df)) {
          paste0("A multivalued CSV dataset (", ncol(vals$multivalued_df), " columns) loaded. Requires binarization/scaling.")
        } else {
          paste0("A formal binary context with ", 
                 if(!is.null(vals$fc$objects)) length(vals$fc$objects) else 0, " objects and ",
                 if(!is.null(vals$fc$attributes)) length(vals$fc$attributes) else 0, " attributes.")
        }
        
        vals$current_doc <- list(
          title = paste0("Imported File: ", input$file$name),
          description = file_desc,
          source = paste0("Local file upload (", toupper(ext), ")"),
          creation_method = paste0(toupper(ext), " Import"),
          timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
          history_log = list(paste0("Imported local file '", input$file$name, "' at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")))
        )

        vals$trigger <- vals$trigger + 1; vals$stats_orig <- NULL; vals$filtered_imps <- NULL
        if(ext != "rds") implications_df(NULL)
      }, error = function(e){ shinyalert("Error", e$message, type = "error") })
    })
  })

  # --- HANDLER PARA GENERACIÓN SINTÉTICA ---
  observeEvent(input$btnGenerateSynthetic, {
    vals$current_doc <- NULL
    req(input$rand_type)
    
    withProgress(message = "Generating synthetic context...", value = 0.5, {
      tryCatch({
        if (input$rand_type == "random_context") {
          req(input$rand_objs, input$rand_atts)
          
          fc_new <- fcaR::RandomContext(
            n_objects = input$rand_objs,
            n_attributes = input$rand_atts,
            density = input$rand_density,
            distribution = input$rand_distribution,
            alpha = if (input$rand_distribution == "dirichlet") input$rand_alpha else 1
          )
          
          vals$fc <- fc_new
          vals$trigger <- vals$trigger + 1
          
          vals$current_doc <- list(
            title = "Synthetic Random Context",
            description = paste0("Randomly generated binary relation context. Parameters: objects=", input$rand_objs, 
                                 ", attributes=", input$rand_atts, ", density=", input$rand_density, 
                                 ", distribution=", input$rand_distribution),
            source = "Generated synthetically inside fcaRviz",
            creation_method = "Synthetic Generation",
            timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
            history_log = list(paste0("Generated random context (", input$rand_objs, "x", input$rand_atts, ") at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")))
          )
          
          reset_context_derived_state()
          removeModal()
          shinyalert("Success", "Synthetic Random Context generated & loaded successfully.", type = "success")
          
        } else if (input$rand_type == "random_distributive") {
          req(input$dist_elements)
          
          fc_new <- fcaR::RandomDistributiveContext(
            n_elements = input$dist_elements,
            density = input$dist_density
          )
          
          vals$fc <- fc_new
          vals$trigger <- vals$trigger + 1
          
          vals$current_doc <- list(
            title = "Synthetic Distributive Context",
            description = paste0("Randomly generated distributive lattice context. Elements=", input$dist_elements, 
                                 ", density=", input$dist_density),
            source = "Generated synthetically inside fcaRviz",
            creation_method = "Distributive Generation",
            timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
            history_log = list(paste0("Generated random distributive context with ", input$dist_elements, " elements at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")))
          )
          
          reset_context_derived_state()
          removeModal()
          shinyalert("Success", "Random Distributive Context generated successfully.", type = "success")
        }
      }, error = function(e) {
        shinyalert("Generation Error", e$message, type = "error")
      })
    })
  })

  # --- HANDLER PARA PERTURBACIÓN / ALEATORIZACIÓN ---
  observeEvent(input$btnPerturbCurrent, {
    req(vals$fc, input$pert_method)
    
    withProgress(message = "Randomizing current context...", value = 0.5, {
      tryCatch({
        iters <- if (is.na(input$pert_iterations) || is.null(input$pert_iterations)) NULL else input$pert_iterations
        
        fc_new <- fcaR::randomize_context(
          fc = vals$fc,
          method = input$pert_method,
          iterations = iters
        )
        
        vals$fc <- fc_new
        vals$trigger <- vals$trigger + 1
        
        # Update current_doc history log
        current_history <- if (!is.null(vals$current_doc$history_log)) vals$current_doc$history_log else list()
        new_log_entry <- paste0("Perturbed context using method: ", input$pert_method, 
                                " (iterations: ", if(is.null(iters)) "auto" else iters, ") at ", 
                                format(Sys.time(), "%Y-%m-%d %H:%M:%S"))
        current_history <- c(current_history, new_log_entry)
        
        if (is.null(vals$current_doc)) {
          vals$current_doc <- list(
            title = "Perturbed/Randomized Context",
            description = paste0("A context generated by randomizing a previous state using method '", input$pert_method, "'."),
            source = "Context perturbation in fcaRviz",
            creation_method = "Randomization / Perturbation",
            timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
            history_log = current_history
          )
        } else {
          vals$current_doc$history_log <- current_history
        }
        
        reset_context_derived_state()
        removeModal()
        shinyalert("Success", "Current context randomized and loaded successfully.", type = "success")
      }, error = function(e) {
        shinyalert("Perturbation Error", e$message, type = "error")
      })
    })
  })

  # --- OBSERVADOR PARA INFORMACIÓN DE GENERADORES SINTÉTICOS ---
  observeEvent(input$btnSyntheticInfo, {
    showModal(modalDialog(
      title = "Synthetic Data Generators Reference Guide",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">Synthetic Data Generation & Perturbation</h5>
          
          <p><strong>1. Random Formal Contexts (Uniform vs. Dirichlet):</strong></p>
          <ul>
            <li><strong>Uniform Distribution:</strong> Generates incidence matrices where each cell has an independent, constant probability of being filled (determined by the density parameter). This yields standard, uncorrelated random structures.</li>
            <li><strong>Dirichlet Distribution:</strong> Mimics real-world datasets by generating attributes with unequal probabilities using a Dirichlet distribution. This models datasets where some attributes are highly frequent and others are rare, introducing more realistic correlation structures.</li>
          </ul>

          <p><strong>2. Random Distributive Contexts (Birkhoff's Theorem):</strong></p>
          <ul>
            <li><strong>Definition:</strong> Generates synthetic datasets that are mathematically guaranteed to produce distributive concept lattices. According to Birkhoff's representation theorem for distributive lattices, any finite distributive lattice is isomorphic to the lattice of down-sets of a partially ordered set (poset). The generator constructs a random poset and uses its order filter relations to populate the incidence matrix.</li>
          </ul>

          <p><strong>3. Context Randomization (Perturbation):</strong></p>
          <ul>
            <li><strong>Edge Swapping (swap):</strong> Swaps incidence values between random cells in a way that preserves the marginal sums (number of attributes per object and objects per attribute). This is useful for statistical significance tests, maintaining density and item frequencies while destroying patterns.</li>
            <li><strong>Rewiring (rewire):</strong> Randomly redistributes incidence values across the entire matrix while preserving the general density of the context.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Bibliographical References</h5>
          <div class=\"p-3 bg-light border rounded mb-3\">
            <ul>
              <li><strong>Birkhoff, G. (1937).</strong> <em>Rings of sets</em>. Duke Mathematical Journal, 3(3), 443-454.</li>
              <li><strong>Ganter, B., & Wille, R. (1999).</strong> <em>Formal Concept Analysis: Mathematical Foundations</em>. Springer-Verlag.</li>
              <li><strong>Snijders, T. A. B. (1991).</strong> <em>Enumeration and simulation methods for 0-1 matrices with given marginals</em>. Psychometrika, 56(3), 397-417.</li>
            </ul>
          </div>
        </div>
      ")
    ))
  })

  output$saveProject <- downloadHandler(
    filename = function() { paste0("fcaR_project_", format(Sys.time(), "%Y%m%d_%H%M"), ".rds") },
    content = function(file) { req(vals$fc); saveRDS(vals$fc, file) }
  )

  output$exportCex <- downloadHandler(
    filename = function() { paste0("fcaR_context_", format(Sys.time(), "%Y%m%d_%H%M"), ".cex") },
    content = function(file) { req(vals$fc); export_cex(vals$fc, file) }
  )

  output$hot_context <- renderRHandsontable({
    vals$trigger; req(vals$fc); mat <- t(as.matrix(vals$fc$I))
    grid_data <- matrix("", nrow = nrow(mat) + 1, ncol = ncol(mat) + 1)
    grid_data[1, 1] <- ""; grid_data[1, 2:ncol(grid_data)] <- colnames(mat)
    grid_data[2:nrow(grid_data), 1] <- rownames(mat)
    bin_data <- apply(mat, c(1,2), function(x) if(x > 0) "X" else "")
    grid_data[2:nrow(grid_data), 2:ncol(grid_data)] <- bin_data
    df_grid <- as.data.frame(grid_data, stringsAsFactors = FALSE)
    custom_renderer <- "function(instance, td, row, col, prop, value, cellProperties) { Handsontable.renderers.TextRenderer.apply(this, arguments); if (row === 0 || col === 0) { td.style.fontWeight = 'bold'; td.style.background = '#f0f0f0'; td.style.textAlign = 'center'; } else { td.style.textAlign = 'center'; } }"
    rhandsontable(df_grid, rowHeaders = NULL, colHeaders = NULL, width = "100%", height = "450px", useTypes = FALSE) %>% hot_cols(renderer = custom_renderer) %>% hot_context_menu(allowRowEdit=TRUE, allowColEdit=TRUE, allowInsertRow=TRUE, allowInsertCol=TRUE, allowRemoveRow=TRUE, allowRemoveCol=TRUE)
  })

  # Save changes automatically when disabling Edit Mode
  observeEvent(input$editMode, {
    if (isFALSE(input$editMode)) {
      req(input$hot_context, vals$fc)
      tryCatch({
        raw_data <- input$hot_context$data
        raw_matrix <- do.call(rbind, lapply(raw_data, function(x) { x[sapply(x, is.null)] <- ""; return(unlist(x)) }))
        new_atts <- as.character(raw_matrix[1, 2:ncol(raw_matrix)]); new_objs <- as.character(raw_matrix[2:nrow(raw_matrix), 1])
        if (any(new_atts == "") || any(is.na(new_atts))) {
          updateMaterialSwitch(session, "editMode", value = TRUE)
          stop("Headers cannot be empty.")
        }
        new_atts <- make.unique(new_atts); new_objs <- make.unique(new_objs)
        data_part <- raw_matrix[2:nrow(raw_matrix), 2:ncol(raw_matrix)]
        mat_bin <- matrix(0, nrow = length(new_objs), ncol = length(new_atts))
        for (i in 1:nrow(data_part)) { 
          for (j in 1:ncol(data_part)) { 
            val <- data_part[i, j]
            if (!is.na(val) && as.character(val) != "" && as.character(val) != "0") { 
              mat_bin[i, j] <- 1 
            } 
          } 
        }
        colnames(mat_bin) <- new_atts; rownames(mat_bin) <- new_objs
        
        current_mat <- t(as.matrix(vals$fc$I))
        if (!identical(current_mat, mat_bin)) {
          vals$fc <- FormalContext$new(mat_bin)
          vals$trigger <- vals$trigger + 1
          implications_df(NULL)
          vals$stats_orig <- NULL
          
          # Append manual edit to history log
          current_history <- if (!is.null(vals$current_doc$history_log)) vals$current_doc$history_log else list()
          new_log_entry <- paste0("Edited context manually (new dimensions: ", length(new_objs), "x", length(new_atts), ") at ", 
                                  format(Sys.time(), "%Y-%m-%d %H:%M:%S"))
          current_history <- c(current_history, new_log_entry)
          
          if (is.null(vals$current_doc)) {
            vals$current_doc <- list(
              title = "Manually Edited Context",
              description = "A formal context created/modified manually by editing cell grid values.",
              source = "Direct User Grid Modification",
              creation_method = "Manual Editing",
              timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
              history_log = current_history
            )
          } else {
            vals$current_doc$history_log <- current_history
          }
          
          shinyalert("Updated!", "Context updated.", type = "success")
        }
      }, error = function(e) {
        updateMaterialSwitch(session, "editMode", value = TRUE)
        shinyalert("Error updating", e$message, type = "error")
      })
    }
  })

  observeEvent(input$btnUpdateContext, {
    updateMaterialSwitch(session, "editMode", value = FALSE)
  })

  # Keep arrow-toggle state in sync for Basic Operations table
  observeEvent(input$showArrowRelations_basic, {
    vals$show_arrows <- isTRUE(input$showArrowRelations_basic)
  }, ignoreNULL = FALSE)

  context_table_dt <- function(fc, page_length = 15, show_arrows = FALSE) {
    d <- get_context_df(fc, show_arrows = show_arrows)
    DT::datatable(
      d,
      rownames = FALSE,
      options = list(scrollX = TRUE, pageLength = page_length, searching = FALSE),
      class = "cell-border stripe"
    )
  }

  output$contents <- DT::renderDT({
    vals$trigger
    req(vals$fc)
    context_table_dt(vals$fc, page_length = 10, show_arrows = FALSE)
  })
  outputOptions(output, "contents", suspendWhenHidden = FALSE)

  observeEvent(input$selectDataset, {
    req(input$selectDataset)
    if(input$selectDataset != "") {
      try({
        vals$fc <- returnFCFromRepo(input$selectDataset)
        vals$trigger <- vals$trigger + 1
        vals$stats_orig <- NULL
        vals$filtered_imps <- NULL
        implications_df(NULL)
        
        # Download and store metadata documentation
        meta_url <- "https://fcarepository.org/contexts.yaml"
        meta_data <- tryCatch(yaml::read_yaml(meta_url), error = function(e) NULL)
        if (!is.null(meta_data) && !is.null(meta_data[[input$selectDataset]])) {
          doc <- meta_data[[input$selectDataset]]
          doc$creation_method <- "Repository Import"
          doc$timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
          doc$history_log <- list(paste0("Loaded repository dataset '", doc$title, "' at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")))
          if (!is.null(doc$source)) {
            doc$source_citation <- doc$source
            doc$source <- "fcaRepository.org (Remote GitHub)"
          }
          vals$current_doc <- doc
        } else {
          vals$current_doc <- NULL
        }
        
        removeModal()
      })
    }
  })

  output$datasetDocumentationUI <- renderUI({
    if (is.null(vals$current_doc)) {
      return(div(
        class = "text-center py-5 text-muted",
        icon("book", class = "fa-4x mb-3 text-muted"),
        h5("No Documentation Available"),
        p("Please import a file, connect to the repository, or generate a context to view documentation/metadata.")
      ))
    }
    
    doc <- vals$current_doc
    
    # Calculate current dimensions dynamically from vals$fc
    cur_objs <- if(!is.null(vals$fc) && !is.null(vals$fc$objects)) length(vals$fc$objects) else 0
    cur_atts <- if(!is.null(vals$fc) && !is.null(vals$fc$attributes)) length(vals$fc$attributes) else 0
    
    # Metadata badges
    lang_badge <- if(!is.null(doc$language)) span(doc$language, class="badge bg-info text-white fs-6") else NULL
    objs_badge <- span(paste0(cur_objs, " objects"), class="badge bg-secondary fs-6")
    atts_badge <- span(paste0(cur_atts, " attributes"), class="badge bg-secondary fs-6")
    
    # History log list rendering
    history_items <- NULL
    if (!is.null(doc$history_log) && length(doc$history_log) > 0) {
      history_items <- tags$ul(
        class = "list-group list-group-flush border rounded mb-3",
        lapply(doc$history_log, function(item) {
          tags$li(class = "list-group-item py-2 px-3 small d-flex align-items-center gap-2",
                  icon("clock", class="text-muted"),
                  span(item))
        })
      )
    }
    
    tagList(
      div(class="border-bottom pb-3 mb-4",
          h3(class="fw-bold text-primary mb-1", if(!is.null(doc$title)) doc$title else "Dataset Metadata"),
          div(class="d-flex gap-2 mt-2", lang_badge, objs_badge, atts_badge)
      ),
      
      # Audit Grid Card
      card(
        class = "mb-4 border-0 bg-light shadow-sm",
        card_body(
          layout_columns(
            col_widths = c(4, 4, 4),
            div(
              h6(class="fw-bold text-uppercase text-muted small mb-1", "Creation Method"),
              p(class="text-dark fw-semibold mb-0", if(!is.null(doc$creation_method)) doc$creation_method else "N/A")
            ),
            div(
              h6(class="fw-bold text-uppercase text-muted small mb-1", "Source Origin"),
              p(class="text-dark mb-0 truncate-text", if(!is.null(doc$source)) doc$source else "N/A", style="max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;")
            ),
            div(
              h6(class="fw-bold text-uppercase text-muted small mb-1", "Creation Time"),
              p(class="text-dark mb-0", if(!is.null(doc$timestamp)) doc$timestamp else "N/A")
            )
          )
        )
      ),
      
      # Description Section
      if(!is.null(doc$description) && doc$description != "") {
        div(class="mb-4",
            h6(class="fw-bold text-uppercase text-muted small mb-2", "Description / Details"),
            p(class="fs-6 text-dark", doc$description)
        )
      } else NULL,
      
      # Bibliography Citation Section
      if(!is.null(doc$source_citation) && doc$source_citation != "") {
        div(class="mb-4",
            h6(class="fw-bold text-uppercase text-muted small mb-2", "Bibliographical Reference / Source"),
            div(class="p-3 bg-light border rounded text-dark fs-6",
                icon("quote-left", class="text-muted me-2"),
                tags$em(doc$source_citation)
            )
        )
      } else NULL,
      
      # History Log Timeline
      if(!is.null(history_items)) {
        div(class="mb-3",
            h6(class="fw-bold text-uppercase text-muted small mb-2", "Audit & History Log"),
            history_items
        )
      } else NULL
    )
  })





  # --- MULTIVALUED SCALING WIZARD SERVER LOGIC ---
  output$scalingWizardUI <- renderUI({
    req(vals$multivalued_df)
    df <- vals$multivalued_df
    cols <- colnames(df)
    
    ui_list <- lapply(cols, function(col) {
      col_data <- df[[col]]
      
      # Intelligent default strategy detection
      default_strategy <- "nominal"
      if (is_binary_vector(col_data)) {
        default_strategy <- "binary"
      } else if (is.numeric(col_data)) {
        default_strategy <- "ordinal"
      }
      
      # Calculate suggested thresholds if numeric
      sugg_thresholds <- ""
      if (is.numeric(col_data)) {
        quants <- quantile(col_data, probs = c(0.25, 0.5, 0.75), na.rm = TRUE)
        sugg_thresholds <- paste(round(unique(quants), 2), collapse = ", ")
      }
      
      # Premium row layout using bslib layout and cards
      card(
        class = "mb-3 p-3 bg-light border-0 shadow-sm",
        style = "border-radius: 8px;",
        div(
          class = "row align-items-center",
          div(
            class = "col-md-3",
            strong(col, class = "text-dark"),
            br(),
            span(class = "badge bg-secondary", 
                 if (is.numeric(col_data)) "Numeric" else if (is_binary_vector(col_data)) "Binary" else "Categorical")
          ),
          div(
            class = "col-md-3",
            selectInput(
              inputId = paste0("scale_type_", col),
              label = "Strategy:",
              choices = c("nominal", "ordinal", "interordinal", "binary", "ignore"),
              selected = default_strategy,
              width = "100%"
            )
          ),
          div(
            class = "col-md-6",
            # Conditional Panel for Thresholds
            conditionalPanel(
              condition = sprintf("input['scale_type_%s'] == 'ordinal' || input['scale_type_%s'] == 'interordinal'", col, col),
              div(
                class = "row",
                conditionalPanel(
                  condition = sprintf("input['scale_type_%s'] == 'ordinal'", col),
                  div(
                    class = "col-6",
                    selectInput(
                      inputId = paste0("scale_dir_", col),
                      label = "Direction:",
                      choices = c("<=" = "le", ">=" = "ge", "both" = "both"),
                      selected = "le",
                      width = "100%"
                    )
                  )
                ),
                div(
                  class = "col-6",
                  textInput(
                    inputId = paste0("scale_thresh_", col),
                    label = "Thresholds (comma-separated):",
                    value = sugg_thresholds,
                    placeholder = "e.g., 10, 20, 30",
                    width = "100%"
                  )
                )
              )
            ),
            conditionalPanel(
              condition = sprintf("input['scale_type_%s'] == 'nominal'", col),
              span(class = "text-muted small", "Nominal scaling will generate one binary attribute per unique category.")
            ),
            conditionalPanel(
              condition = sprintf("input['scale_type_%s'] == 'binary'", col),
              span(class = "text-muted small", "Binary values will be imported directly as-is.")
            ),
            conditionalPanel(
              condition = sprintf("input['scale_type_%s'] == 'ignore'", col),
              span(class = "text-danger small", "This column will be excluded from the formal context.")
            )
          )
        )
      )
    })
    
    do.call(tagList, ui_list)
  })

  observeEvent(input$btnPerformScaling, {
    req(vals$multivalued_df)
    df <- vals$multivalued_df
    cols <- colnames(df)
    config <- list()
    
    for (col in cols) {
      type <- input[[paste0("scale_type_", col)]]
      if (is.null(type)) next
      
      thresh_val <- input[[paste0("scale_thresh_", col)]]
      thresholds <- NULL
      if (!is.null(thresh_val) && nzchar(trimws(thresh_val))) {
        # Parse comma separated values
        parsed <- as.numeric(unlist(strsplit(thresh_val, ",")))
        thresholds <- parsed[!is.na(parsed)]
      }
      
      dir_val <- input[[paste0("scale_dir_", col)]]
      
      config[[col]] <- list(
        type = type,
        thresholds = thresholds,
        direction = dir_val
      )
    }
    
    withProgress(message = 'Scaling and importing context...', value = 0.5, {
      tryCatch({
        scaled_df <- perform_conceptual_scaling(df, config)
        
        # Convert to FormalContext
        scaled_mat <- as.matrix(scaled_df)
        vals$fc <- FormalContext$new(scaled_mat)
        vals$trigger <- vals$trigger + 1
        
        # Populate scaling metadata
        vals$current_doc <- list(
          title = paste0("Scaled Context: ", if(!is.null(vals$current_doc$title)) vals$current_doc$title else "Imported Dataset"),
          description = paste0("Created via Formal Conceptual Scaling of multivalued columns. Configured attributes scaled into ", ncol(scaled_df), " binary features."),
          source = if(!is.null(vals$current_doc$source)) vals$current_doc$source else "Conceptual scaling pipeline",
          creation_method = "Conceptual Scaling",
          timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
          history_log = c(
            if(!is.null(vals$current_doc$history_log)) vals$current_doc$history_log else list(),
            paste0("Applied conceptual scaling on multivalued dataset to generate ", ncol(scaled_df), " attributes at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S"))
          )
        )
        
        vals$stats_orig <- NULL
        vals$filtered_imps <- NULL
        implications_df(NULL)
        
        shinyalert("Success", "Conceptual scaling complete! Formal context updated.", type = "success")
        
        # Go back to Viewer tab
        nav_select("data_management_tabs", selected = "Context Viewer & Editor")
      }, error = function(e) {
        shinyalert("Scaling Error", e$message, type = "error")
      })
    })
  })

  # ===========================================================================
  # 3. OPERACIONES BÁSICAS
  # ===========================================================================
  output$tableData_DT <- DT::renderDT({
    vals$trigger
    vals$show_arrows
    req(vals$fc)
    context_table_dt(vals$fc, show_arrows = isTRUE(vals$show_arrows))
  })
  output$formalContext <- renderText({ vals$trigger; req(vals$fc); paste(capture.output(print(vals$fc)), collapse = "\n") })
  observeEvent(input$clarify, { req(vals$fc); vals$fc$clarify(); vals$trigger <- vals$trigger + 1; reset_context_derived_state() })
  observeEvent(input$reduce, { req(vals$fc); vals$fc$reduce(); vals$trigger <- vals$trigger + 1; reset_context_derived_state() })
  observeEvent(input$reduceArrows, {
    req(vals$fc)
    if (!has_arrow_relations_api(vals$fc)) {
      shinyalert("Not available", "reduce_arrows() requires fcaR >= 1.7.0.", type = "warning")
      return()
    }
    if (!is_binary_formal_context(vals$fc)) {
      shinyalert("Binary context required", "Arrow reduction only applies to binary formal contexts.", type = "warning")
      return()
    }
    tryCatch({
      n_objs_before <- length(vals$fc$objects)
      n_atts_before <- length(vals$fc$attributes)
      vals$fc <- vals$fc$reduce_arrows()
      vals$trigger <- vals$trigger + 1
      reset_context_derived_state()
      shinyalert(
        "Context reduced",
        sprintf(
          "Arrow-based reduction: %d → %d objects, %d → %d attributes.",
          n_objs_before, length(vals$fc$objects),
          n_atts_before, length(vals$fc$attributes)
        ),
        type = "success"
      )
    }, error = function(e) shinyalert("Reduce arrows failed", conditionMessage(e), type = "error"))
  })
  output$intentResult <- renderUI({ req(input$intentOptions, vals$fc); S <- Set$new(vals$fc$objects); sapply(input$intentOptions, function(x) do.call(S$assign, setNames(list(1), x))); res <- vals$fc$intent(S); HTML(paste("<pre>", paste(capture.output(print(res)), collapse = "\n"), "</pre>")) })
  output$extentResult <- renderUI({ req(input$extentOptions, vals$fc); S <- Set$new(vals$fc$attributes); sapply(input$extentOptions, function(x) do.call(S$assign, setNames(list(1), x))); res <- vals$fc$extent(S); HTML(paste("<pre>", paste(capture.output(print(res)), collapse = "\n"), "</pre>")) })
  output$closureResult <- renderUI({ req(input$closureOptions, vals$fc); S <- Set$new(vals$fc$attributes); sapply(input$closureOptions, function(x) do.call(S$assign, setNames(list(1), x))); res <- vals$fc$closure(S); HTML(paste("<pre>", paste(capture.output(print(res)), collapse = "\n"), "</pre>")) })
  observeEvent(input$createLatex, { showModal(modalDialog(title = "LaTeX Code", verbatimTextOutput("latexTable"), easyClose = TRUE)) })
  output$latexTable <- renderText({ req(vals$fc); vals$fc$to_latex() })
  output$downloadRds <- downloadHandler(filename = function() { "context.rds" }, content = function(file) { vals$fc$save(filename = "./fc.rds"); file.copy("./fc.rds", file) })
  output$downloadReport <- downloadHandler(filename = function() { paste("fca_report_", Sys.Date(), ".html", sep="") }, content = function(file) { req(vals$fc); id <- showNotification("Generating...", duration = NULL, closeButton = FALSE); on.exit(removeNotification(id), add = TRUE); tempReport <- file.path(tempdir(), "report.Rmd"); if(!file.exists("report.Rmd")) { writeLines(c("---", "title: 'Report'", "output: html_document", "---", "# Report", "Context loaded."), tempReport) } else { file.copy("report.Rmd", tempReport, overwrite = TRUE) }; rmarkdown::render(tempReport, output_file = file, params = list(fc = vals$fc), envir = new.env(parent = globalenv())) })

  # ===========================================================================
  # 4. CONCEPTOS Y RETÍCULO
  # ===========================================================================
  observeEvent(input$main_nav, {
    if (input$main_nav == "ui_concepts") {
      vals$trigger
      req(vals$fc)
      ready <- tryCatch(!vals$fc$concepts$is_empty(), error = function(e) FALSE)
      if (!ready) {
        if (isTRUE(input$ask_before_calc)) {
          showModal(modalDialog(
            title = "Compute Concepts?",
            "The concept lattice has not been computed yet for this context.",
            easyClose = TRUE,
            footer = tagList(
              input_task_button("getConcepts", "Compute Now"),
              actionButton("goBackConc", "Cancel")
            )
          ))
        } else {
          # Calculate automatically in background
          bg_calc$start_time <- Sys.time()
          bg_calc$task_type <- "concepts"
          bg_calc$process <- callr::r_bg(function(fc) {
            library(fcaR)
            fc$find_concepts()
            return(fc)
          }, args = list(fc = vals$fc))
        }
      }
    }
  })
  observeEvent(input$getConcepts, {
    req(vals$fc)
    removeModal()
    bg_calc$start_time <- Sys.time()
    bg_calc$task_type <- "concepts"
    bg_calc$process <- callr::r_bg(function(fc) {
      library(fcaR)
      fc$find_concepts()
      return(fc)
    }, args = list(fc = vals$fc))
  })
  observeEvent(input$goBackConc, { updateRadioGroupButtons(session, "main_nav", selected = "basic_operations"); removeModal() })

  observeEvent(input$btnForceRender, {
    req(vals$fc)
    vals$force_interactive_render <- TRUE
  })

  # --- ACTIVE SELECTED CONCEPT ID STATE & SYNCHRONIZATION ---
  vals$active_concept_id <- 1
  vals$navigation_history <- NULL
  vals$original_nodes_df <- NULL

  # Cache original styles whenever FormalContext is updated
  observeEvent(vals$fc, {
    req(vals$fc)
    if (!tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) {
      n <- vals$fc$concepts$size()
      thresh <- 800
      if (!is.null(input$enable_threshold)) {
        thresh <- if (isTRUE(input$enable_threshold)) (if (!is.null(input$safety_threshold)) input$safety_threshold else 800) else Inf
      }
      if (n > 0 && n <= thresh) {
        try({
          g_data <- getGraph(vals$fc$concepts, vals$fc)
          vals$original_nodes_df <- data.frame(
            id = as.character(g_data$nodes$id),
            color.background = g_data$nodes$color.background,
            color.border = g_data$nodes$color.border,
            size = rep(22, nrow(g_data$nodes)),
            borderWidth = if ("borderWidth" %in% names(g_data$nodes)) g_data$nodes$borderWidth else 1,
            stringsAsFactors = FALSE
          )
        }, silent = TRUE)
      } else {
        vals$original_nodes_df <- NULL
      }
    } else {
      vals$original_nodes_df <- NULL
    }
  })

  # --- CONCEPT FILTERING LOGIC ---
  # Reset filters on new context or concepts computed
  observeEvent(vals$trigger, {
    vals$concept_filter_ids <- NULL
    updateSelectizeInput(session, "filterObjs", selected = character(0))
    updateSelectizeInput(session, "filterAtts", selected = character(0))
  })

  # Helper function to calculate filtered concept IDs
  calculate_filtered_concept_ids <- function(fc, sel_objs, sel_atts) {
    req(fc)
    if (tryCatch(fc$concepts$is_empty(), error = function(e) TRUE)) return(NULL)
    
    n_concepts <- fc$concepts$size()
    if (n_concepts == 0) return(integer(0))
    
    # Start with all concept IDs
    matching_ids <- 1:n_concepts
    
    # If no filters applied, return all IDs
    if (length(sel_objs) == 0 && length(sel_atts) == 0) {
      return(matching_ids)
    }
    
    # Get extents and intents matrices
    exts <- as.matrix(fc$concepts$extents())
    ints <- as.matrix(fc$concepts$intents())
    
    # Rownames of exts are object names, rownames of ints are attribute names
    rownames(exts) <- fc$objects
    rownames(ints) <- fc$attributes
    
    keep_by_objs <- rep(TRUE, n_concepts)
    if (length(sel_objs) > 0) {
      # Keep concepts where all selected objects are present in the extent
      keep_by_objs <- colSums(exts[sel_objs, , drop = FALSE]) == length(sel_objs)
    }
    
    keep_by_atts <- rep(TRUE, n_concepts)
    if (length(sel_atts) > 0) {
      # Keep concepts where all selected attributes are present in the intent
      keep_by_atts <- colSums(ints[sel_atts, , drop = FALSE]) == length(sel_atts)
    }
    
    matching_ids <- which(keep_by_objs & keep_by_atts)
    return(matching_ids)
  }

  # Apply button
  observeEvent(input$btnApplyConceptFilters, {
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) {
      shinyalert("Oops!", "Please compute concepts first.", type = "warning")
      return()
    }
    
    # Calculate filtered IDs
    filtered <- calculate_filtered_concept_ids(vals$fc, input$filterObjs, input$filterAtts)
    vals$concept_filter_ids <- filtered
    vals$tab_concept_filter_ids <- filtered
    
    # Synchronize selectize inputs in the other tab
    updateSelectizeInput(session, "filterObjsTab", choices = vals$fc$objects, selected = input$filterObjs)
    updateSelectizeInput(session, "filterAttsTab", choices = vals$fc$attributes, selected = input$filterAtts)
    
    # Auto-focus synchronization
    if (!is.null(filtered) && length(filtered) > 0) {
      curr_active <- vals$active_concept_id
      if (!(curr_active %in% filtered)) {
        # Find the one with maximum extent size (largest extent colSum)
        exts <- as.matrix(vals$fc$concepts$extents())
        filtered_sizes <- colSums(exts[, filtered, drop = FALSE])
        new_active <- filtered[which.max(filtered_sizes)]
        
        vals$active_concept_id <- new_active
        # Update the visNetwork selected node if in macro mode
        if (isTRUE(input$exploration_mode == "macro")) {
          visNetwork::visNetworkProxy("interactivePlot") %>%
            visNetwork::visSelectNodes(id = as.character(new_active))
        } else {
          # micro mode: update the center concept
          updateNumericInput(session, "center_concept_id", value = new_active)
        }
      }
    }
  })

  # Clear button
  observeEvent(input$btnClearConceptFilters, {
    req(vals$fc)
    updateSelectizeInput(session, "filterObjs", choices = vals$fc$objects, selected = character(0))
    updateSelectizeInput(session, "filterAtts", choices = vals$fc$attributes, selected = character(0))
    updateSelectizeInput(session, "filterObjsTab", choices = vals$fc$objects, selected = character(0))
    updateSelectizeInput(session, "filterAttsTab", choices = vals$fc$attributes, selected = character(0))
    vals$concept_filter_ids <- NULL
    vals$tab_concept_filter_ids <- NULL
  })

  # Reactive to get the current filtered IDs
  current_filtered_ids <- reactive({
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) return(NULL)
    
    n_concepts <- vals$fc$concepts$size()
    if (isTRUE(input$bypassFilters) || is.null(vals$concept_filter_ids)) {
      return(1:n_concepts)
    } else {
      return(vals$concept_filter_ids)
    }
  })

  # Render status badge
  output$filterStatusBadge <- renderText({
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) return("")
    
    n_total <- vals$fc$concepts$size()
    if (isTRUE(input$bypassFilters)) {
      return(paste("Showing all", n_total, "concepts (Bypass active)"))
    }
    if (is.null(vals$concept_filter_ids)) {
      return(paste("Showing all", n_total, "concepts"))
    } else {
      n_filtered <- length(vals$concept_filter_ids)
      return(paste("Showing", n_filtered, "of", n_total, "concepts"))
    }
  })

  observeEvent(input$interactivePlot_selected, {
    req(vals$fc)
    n <- vals$fc$concepts$size()
    
    # Selection cleared: Restore original styles
    if (is.null(input$interactivePlot_selected) || input$interactivePlot_selected == "") {
      if (!is.null(vals$original_nodes_df) && isTRUE(input$exploration_mode == "macro")) {
        try({
          # Get current filtered ids
          curr_ids <- as.character(current_filtered_ids())
          df_to_update <- vals$original_nodes_df[vals$original_nodes_df$id %in% curr_ids, ]
          if (nrow(df_to_update) > 0) {
            visNetwork::visNetworkProxy("interactivePlot") %>%
              visNetwork::visUpdateNodes(nodes = df_to_update)
          }
        }, silent = TRUE)
      }
      return()
    }
    
    # Node selected
    C_0 <- as.numeric(input$interactivePlot_selected)
    req(C_0 > 0 && C_0 <= n)
    vals$active_concept_id <- C_0
    show_lattice_node_info(C_0, pin = TRUE)
    
    # Compute and highlight Ideal & Filter (ConExp style)
    thresh <- 800
    if (!is.null(input$enable_threshold)) {
      thresh <- if (isTRUE(input$enable_threshold)) (if (!is.null(input$safety_threshold)) input$safety_threshold else 800) else Inf
    }
    if (isTRUE(input$exploration_mode == "macro") && n <= thresh) {
      try({
        intents_mat <- as.matrix(vals$fc$concepts$intents())
        c_int <- intents_mat[, C_0]
        
        # Filter: superconcepts (intent(C) subset of intent(C_0))
        is_super <- colSums(intents_mat & !c_int) == 0
        filter_ids <- which(is_super)
        
        # Ideal: subconcepts (intent(C_0) subset of intent(C))
        is_sub <- colSums(c_int & !intents_mat) == 0
        ideal_ids <- which(is_sub)
        
        # Preserve the active color palette (irreducibles or metric).  Ideal,
        # filter and selection are indicated with borders and size only.
        node_ids <- as.character(seq_len(n))
        base_styles <- vals$original_nodes_df
        if (is.null(base_styles) || nrow(base_styles) == 0) {
          base_graph <- getGraph(
            vals$fc$concepts,
            vals$fc,
            filter_ids = current_filtered_ids(),
            scale_support = isTRUE(input$scale_node_support),
            labeling_mode = if (!is.null(input$labeling_mode)) input$labeling_mode else "reduced",
            color_mode = if (!is.null(input$color_mode)) input$color_mode else "irreducibles",
            metrics = tryCatch(concepts_metrics(), error = function(e) NULL)
          )
          base_styles <- data.frame(
            id = as.character(base_graph$nodes$id),
            color.background = base_graph$nodes$color.background,
            color.border = base_graph$nodes$color.border,
            size = if ("size" %in% names(base_graph$nodes)) base_graph$nodes$size else 22,
            borderWidth = if ("borderWidth" %in% names(base_graph$nodes)) base_graph$nodes$borderWidth else 1,
            stringsAsFactors = FALSE
          )
        }
        cached_idx <- match(node_ids, base_styles$id)
        bg_colors <- base_styles$color.background[cached_idx]
        bg_colors[is.na(bg_colors)] <- "rgba(220, 220, 220, 0.25)"
        border_colors <- base_styles$color.border[cached_idx]
        border_colors[is.na(border_colors)] <- "#666666"
        sizes <- base_styles$size[cached_idx]
        sizes[is.na(sizes)] <- 22
        border_widths <- rep(1, n)
        
        # Filter (Blue)
        border_colors[filter_ids] <- "#2E86C1"
        border_widths[filter_ids] <- 2
        
        # Ideal (Green)
        border_colors[ideal_ids] <- "#239B56"
        border_widths[ideal_ids] <- 2
        
        # Selected Concept (Red/Orange)
        border_colors[C_0] <- "#922B21"
        sizes[C_0] <- max(sizes[C_0] + 5, 27)
        border_widths[C_0] <- 4
        
        nodes_df <- data.frame(
          id = node_ids,
          color.background = bg_colors,
          color.border = border_colors,
          size = sizes,
          borderWidth = border_widths,
          stringsAsFactors = FALSE
        )
        
        # Subset nodes_df to only include current filtered concept IDs
        curr_ids <- as.character(current_filtered_ids())
        nodes_df <- nodes_df[nodes_df$id %in% curr_ids, ]
        
        visNetwork::visNetworkProxy("interactivePlot") %>%
          visNetwork::visUpdateNodes(nodes = nodes_df)
      }, silent = TRUE)
    }
  })
  
  observeEvent(input$clicked_macro_node, {
    req(input$clicked_macro_node)
    node_id <- parse_lattice_node_id(input$clicked_macro_node)
    req(!is.na(node_id))
    vals$active_concept_id <- node_id
    show_lattice_node_info(node_id, pin = TRUE)
    bslib::accordion_panel_open("lattice_sidebar_accordion", "Selected Concept")
  })

  observeEvent(input$clicked_micro_node, {
    req(input$clicked_micro_node)
    new_id <- parse_lattice_node_id(input$clicked_micro_node)
    req(!is.na(new_id))
    vals$active_concept_id <- new_id
    updateNumericInput(session, "center_concept_id", value = new_id)
    show_lattice_node_info(new_id, pin = TRUE)
    bslib::accordion_panel_open("lattice_sidebar_accordion", "Selected Concept")
  })
  
  observeEvent(input$center_concept_id, {
    req(input$center_concept_id)
    if (isTRUE(input$exploration_mode == "micro")) {
      vals$active_concept_id <- as.numeric(input$center_concept_id)
    }
  })

  observeEvent(input$exploration_mode, {
    req(input$exploration_mode)
    if (input$exploration_mode == "micro") {
      curr_active <- if (!is.null(vals$active_concept_id)) vals$active_concept_id else 1
      updateNumericInput(session, "center_concept_id", value = curr_active)
    } else if (input$exploration_mode == "macro") {
      curr_active <- if (!is.null(vals$active_concept_id)) vals$active_concept_id else 1
      # Programmatically select the active node in macro view
      try({
        visNetwork::visNetworkProxy("interactivePlot") %>%
          visNetwork::visSelectNodes(id = as.character(curr_active))
      }, silent = TRUE)
    }
  })

  observeEvent(vals$active_concept_id, {
    req(vals$active_concept_id, vals$fc)
    n <- vals$fc$concepts$size()
    req(vals$active_concept_id > 0 && vals$active_concept_id <= n)
    
    if (is.null(vals$navigation_history)) {
      vals$navigation_history <- vals$active_concept_id
    } else {
      last <- tail(vals$navigation_history, 1)
      if (last != vals$active_concept_id) {
        vals$navigation_history <- c(vals$navigation_history, vals$active_concept_id)
      }
    }
  })

  observeEvent(input$btnClearHistory, {
    vals$navigation_history <- vals$active_concept_id
  })

  observeEvent(input$jump_concept_id, {
    req(input$jump_concept_id)
    new_id <- as.numeric(input$jump_concept_id)
    vals$active_concept_id <- new_id
    if (isTRUE(input$exploration_mode == "micro")) {
      updateNumericInput(session, "center_concept_id", value = new_id)
    } else {
      visNetwork::visNetworkProxy("interactivePlot") %>%
        visNetwork::visSelectNodes(id = as.character(new_id))
    }
  })

  # --- LATTICE INFO PANEL & VIEW CONTROLS ---
  set_lattice_info_panel <- function(title, body_html, kind = "concept", pin = FALSE) {
    vals$lattice_info_title <- title
    vals$lattice_info_body <- body_html
    vals$lattice_info_kind <- kind
    vals$lattice_info_visible <- TRUE
    vals$lattice_info_pinned <- isTRUE(pin)
    vals$lattice_info_nonce <- vals$lattice_info_nonce + 1L
    invisible(NULL)
  }

  clear_lattice_info_panel <- function() {
    vals$lattice_info_visible <- FALSE
    vals$lattice_info_pinned <- FALSE
    vals$lattice_info_title <- NULL
    vals$lattice_info_body <- NULL
    vals$lattice_info_kind <- "concept"
    vals$lattice_info_nonce <- vals$lattice_info_nonce + 1L
    invisible(NULL)
  }

  show_lattice_node_info <- function(node_id, pin = FALSE) {
    if (is.null(vals$fc) || is.null(node_id)) return(invisible(NULL))
    node_id <- as.numeric(node_id)
    n <- vals$fc$concepts$size()
    if (is.na(node_id) || node_id < 1 || node_id > n) return(invisible(NULL))

    dt <- tryCatch(
      get_concepts_dataframe(vals$fc$concepts$sub(node_id), vals$fc),
      error = function(e) get_concepts_dataframe(vals$fc$concepts[node_id], vals$fc)
    )
    col_mode <- if (!is.null(input$color_mode)) input$color_mode else "irreducibles"
    met <- tryCatch(concepts_metrics(), error = function(e) NULL)

    labels_val <- ""
    status <- tryCatch(get_concept_irreducible_status(node_id, vals$fc$concepts, vals$fc), error = function(e) NULL)
    if (!is.null(status) && length(status$introduced_attributes) > 0) {
      labels_val <- paste(status$introduced_attributes, collapse = ", ")
    } else if (nrow(dt) > 0 && nzchar(dt$attributes[1])) {
      labels_val <- dt$attributes[1]
    }

    body_html <- get_concept_tooltip(
      node_id, labels_val, dt$objects[1], dt$attributes[1], col_mode, met, concepts = vals$fc$concepts
    )
    set_lattice_info_panel(
      title = paste0("C", node_id),
      body_html = body_html,
      kind = "concept",
      pin = pin
    )
    vals$active_edge_from <- NULL
    vals$active_edge_to <- NULL
    vals$active_edge_nonce <- vals$active_edge_nonce + 1L
  }

  show_lattice_edge_info <- function(edge_key = NULL, edge_from = NULL, edge_to = NULL, pin = FALSE) {
    if (is.null(vals$fc)) return(invisible(NULL))

    from_id <- if (!is.null(edge_from)) as.character(edge_from) else NULL
    to_id <- if (!is.null(edge_to)) as.character(edge_to) else NULL

    if ((is.null(from_id) || !nzchar(from_id) || is.null(to_id) || !nzchar(to_id)) &&
        !is.null(vals$lattice_edges_cache) && nrow(vals$lattice_edges_cache) > 0) {
      edges <- vals$lattice_edges_cache
      idx <- integer(0)
      if (!is.null(edge_key) && nzchar(as.character(edge_key))) {
        edge_key <- as.character(edge_key)
        idx <- which(
          paste0("e:", edges$from, ":", edges$to) == edge_key |
            paste(edges$from, edges$to, sep = "-") == edge_key |
            paste(edges$from, edges$to, sep = "|") == edge_key |
            as.character(edges$id) == edge_key
        )
      }
      if (length(idx) > 0) {
        row <- edges[idx[1], , drop = FALSE]
        from_id <- as.character(row$from)
        to_id <- as.character(row$to)
      }
    }

    if (is.null(from_id) || !nzchar(from_id) || is.null(to_id) || !nzchar(to_id)) {
      return(invisible(NULL))
    }

    title_txt <- paste0("C", from_id, " \u2192 C", to_id)
    body_html <- build_lattice_association_rule_html(from_id, to_id, vals$fc)

    set_lattice_info_panel(
      title = title_txt,
      body_html = body_html,
      kind = "edge",
      pin = pin
    )
    vals$active_edge_from <- from_id
    vals$active_edge_to <- to_id
    vals$active_edge_nonce <- vals$active_edge_nonce + 1L
  }

  lattice_viz_control <- function(action) {
    session$sendCustomMessage("latticeVizControl", list(action = action))
  }

  observeEvent(input$latticeZoomIn, { lattice_viz_control("zoomIn") })
  observeEvent(input$latticeZoomOut, { lattice_viz_control("zoomOut") })
  observeEvent(input$latticeFit, { lattice_viz_control("fit") })
  observeEvent(input$latticeMoveUp, { lattice_viz_control("moveUp") })
  observeEvent(input$latticeMoveDown, { lattice_viz_control("moveDown") })
  observeEvent(input$latticeMoveLeft, { lattice_viz_control("moveLeft") })
  observeEvent(input$latticeMoveRight, { lattice_viz_control("moveRight") })
  observeEvent(input$btnLatticeTrueFullscreen, {
    session$sendCustomMessage("latticeToggleFullscreen", list())
  }, ignoreInit = TRUE)

  observeEvent(input$btnCloseLatticeInfo, {
    clear_lattice_info_panel()
  }, ignoreNULL = TRUE)

  parse_lattice_node_id <- function(x) {
    if (is.null(x)) return(NA_real_)
    if (is.list(x) && !is.null(x$id)) return(as.numeric(x$id))
    as.numeric(x)
  }

  parse_lattice_edge_event <- function(x) {
    if (is.null(x)) {
      return(list(key = NULL, from = NULL, to = NULL))
    }
    if (is.list(x) || (is.vector(x) && !is.null(names(x)))) {
      key <- x[["key"]]
      if (is.null(key)) key <- x[["id"]]
      from <- x[["from"]]
      to <- x[["to"]]
      return(list(
        key = if (!is.null(key)) as.character(key) else NULL,
        from = if (!is.null(from)) as.character(from) else NULL,
        to = if (!is.null(to)) as.character(to) else NULL
      ))
    }
    list(key = as.character(x), from = NULL, to = NULL)
  }

  observeEvent(input$lattice_hover_node, {
    if (isTRUE(vals$lattice_info_pinned)) return()
    node_id <- parse_lattice_node_id(input$lattice_hover_node)
    if (is.na(node_id)) return()
    show_lattice_node_info(node_id, pin = FALSE)
  }, ignoreNULL = FALSE)

  observeEvent(input$lattice_hover_edge, {
    if (isTRUE(vals$lattice_info_pinned)) return()
    edge <- parse_lattice_edge_event(input$lattice_hover_edge)
    if ((is.null(edge$from) || !nzchar(edge$from) || is.null(edge$to) || !nzchar(edge$to)) &&
        (is.null(edge$key) || !nzchar(edge$key))) {
      return()
    }
    show_lattice_edge_info(
      edge_key = edge$key,
      edge_from = edge$from,
      edge_to = edge$to,
      pin = FALSE
    )
  }, ignoreNULL = FALSE)

  observeEvent(input$lattice_click_node, {
    node_id <- parse_lattice_node_id(input$lattice_click_node)
    req(!is.na(node_id))
    vals$active_concept_id <- node_id
    show_lattice_node_info(node_id, pin = TRUE)
    if (isTRUE(input$exploration_mode == "micro")) {
      if (is.null(input$center_concept_id) || as.numeric(input$center_concept_id) != node_id) {
        updateNumericInput(session, "center_concept_id", value = node_id)
      }
    }
    bslib::accordion_panel_open("lattice_sidebar_accordion", "Selected Concept")
  }, ignoreNULL = TRUE)

  observeEvent(input$lattice_click_edge, {
    edge <- parse_lattice_edge_event(input$lattice_click_edge)
    req(
      (!is.null(edge$from) && nzchar(edge$from) && !is.null(edge$to) && nzchar(edge$to)) ||
        (!is.null(edge$key) && nzchar(edge$key))
    )
    show_lattice_edge_info(
      edge_key = edge$key,
      edge_from = edge$from,
      edge_to = edge$to,
      pin = TRUE
    )
    bslib::accordion_panel_open("lattice_sidebar_accordion", "Association Rule")
  }, ignoreNULL = TRUE)

  output$latticeNodeInfoPanel <- renderUI({
    vals$lattice_info_nonce
    if (!isTRUE(vals$lattice_info_visible)) return(NULL)
    req(vals$lattice_info_title, vals$lattice_info_body)
    build_lattice_info_panel(
      title = vals$lattice_info_title,
      body_html = vals$lattice_info_body,
      kind = if (!is.null(vals$lattice_info_kind)) vals$lattice_info_kind else "concept"
    )
  })
  outputOptions(output, "latticeNodeInfoPanel", suspendWhenHidden = FALSE)

  # --- RENDER LATTICE CONTAINER ---
  output$latticeContainer <- renderUI({
    vals$trigger
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) return(NULL)
    
    n_total <- vals$fc$concepts$size()
    curr_ids <- current_filtered_ids()
    n_filtered <- if (is.null(curr_ids)) n_total else length(curr_ids)
    
    style_tag <- tags$style(HTML("
      #latticeContainer {
        height: 100% !important;
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-height: 0 !important;
      }
      #interactivePlot {
        height: 100% !important;
        flex: 1 1 auto;
        min-height: 0 !important;
      }
      .shiny-spinner-placeholder {
        height: 100% !important;
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        min-height: 0 !important;
      }
    "))
    
    if (isTRUE(input$exploration_mode == "micro")) {
      return(tagList(style_tag, visNetworkOutput("interactivePlot", height = "100%")))
    }
    
    if (n_filtered > 5000) {
      return(div(class="alert alert-danger", 
                 tags$h4(tags$strong("⚠️ Visual Bottleneck: Lattice Too Large")),
                 p(paste("The filtered lattice contains", n_filtered, "concepts, which exceeds the absolute safety threshold of 5000 nodes.")),
                 p("To protect your browser from freezing, the full macro visualization is locked."),
                 p(class="fw-bold text-success", "However, you can explore the lattice safely and instantly using the Micro Explorer!"),
                 tags$ul(
                   tags$li("Switch to ", tags$strong("Micro Explorer"), " mode at the top right of this card."),
                   tags$li("Explore neighborhoods locally by clicking on parent and child nodes.")
                 )
             ))
    }
    
    thresh <- 800
    if (!is.null(input$enable_threshold)) {
      thresh <- if (isTRUE(input$enable_threshold)) (if (!is.null(input$safety_threshold)) input$safety_threshold else 800) else Inf
    }
    if (n_filtered > thresh) {
      if (vals$force_interactive_render) {
        return(tagList(style_tag, visNetworkOutput("interactivePlot", height = "100%")))
      } else {
        return(tagList(
          div(class="alert alert-warning",
              tags$h4(tags$strong("⚠️ Warning: Large Lattice Detected")),
              p(paste("This concept lattice contains", n_filtered, "concepts (filtered). Rendering the full interactive lattice automatically may cause your browser to lag or freeze (Visual Bottleneck).")),
              p("To explore this data safely, we recommend:"),
              tags$ul(
                tags$li("Reducing the formal context using ", tags$strong("Clarify"), " or ", tags$strong("Reduce"), " in the Basic Operations section."),
                tags$li("Using targeted attribute filters or sub-posets to focus on specific zones."),
                tags$li("Exploring via the ", tags$strong("Micro Explorer"), " (focal navigation mode).")
              ),
              p("Alternatively, you can view the static plot below or force the full interactive layout if you are on a high-performance system:"),
              div(class="d-flex gap-2 mt-2",
                  actionButton("btnForceRender", "Force Interactive Render Anyway", class="btn-warning btn-sm", icon = icon("play")),
                  downloadButton("downloadStaticPlot", "Download Static Plot (PDF)", class="btn-outline-secondary btn-sm")
              )
          ),
          h5("Static Concept Lattice Plot"),
          plotOutput("staticPlot", height="700px")
        ))
      }
    }
    
    # Default case: n_filtered <= threshold
    tagList(style_tag, visNetworkOutput("interactivePlot", height = "100%"))
  })

  # --- CONCEPT METRICS REACTIVE ---
  concepts_metrics <- reactive({
    vals$trigger
    req(vals$fc)
    ready <- tryCatch(!vals$fc$concepts$is_empty(), error = function(e) FALSE)
    req(ready)
    
    concepts <- vals$fc$concepts
    n_size <- concepts$size()
    
    if (n_size > 100) {
      withProgress(message = "Computing Concept Metrics...", value = 0.1, {
        setProgress(detail = "Calculating Stability...")
        stab <- tryCatch(concepts$stability(), error = function(e) rep(NA, n_size))
        
        setProgress(value = 0.5, detail = "Calculating Separation...")
        sep <- tryCatch(concepts$separation(), error = function(e) rep(NA, n_size))
        
        setProgress(value = 0.8, detail = "Calculating Density...")
        dens <- tryCatch(concepts$density(vals$fc$I), error = function(e) rep(NA, n_size))
      })
    } else {
      stab <- tryCatch(concepts$stability(), error = function(e) rep(NA, n_size))
      sep <- tryCatch(concepts$separation(), error = function(e) rep(NA, n_size))
      dens <- tryCatch(concepts$density(vals$fc$I), error = function(e) rep(NA, n_size))
    }
    
    list(stability = stab, separation = sep, density = dens)
  })

  # --- DYNAMIC LATTICE LEGEND ---
  output$latticeLegendUI <- renderUI({
    vals$trigger
    req(vals$fc)
    ready <- tryCatch(!vals$fc$concepts$is_empty(), error = function(e) FALSE)
    if (!ready) return(NULL)
    
    color_mode <- if (!is.null(input$color_mode)) input$color_mode else "irreducibles"
    
    if (color_mode == "irreducibles") {
      div(
        class = "d-flex align-items-center gap-2",
        span(style="color:#FFD580; font-weight:900; margin-left:10px;", "● Attribute (meet-irreducible)"),
        span(style="color:#97C2FC; font-weight:900; margin-left:10px;", "● Object (join-irreducible)"),
        span(style="color:#90EE90; font-weight:900; margin-left:10px;", "● Both (meet + join)"),
        span(style="color:#E0E0E0; font-weight:900; margin-left:10px;", "● Intersection")
      )
    } else {
      # Metrics legend with gradient scale
      met <- tryCatch(concepts_metrics(), error = function(e) NULL)
      if (is.null(met)) return(NULL)
      
      low_col <- ""
      high_col <- ""
      vals_vec <- NULL
      label_text <- ""
      
      if (color_mode == "stability") {
        label_text <- "Stability"
        low_col <- "#F3E5F5"
        high_col <- "#4A148C"
        vals_vec <- met$stability
      } else if (color_mode == "separation") {
        label_text <- "Separation"
        low_col <- "#E0F2F1"
        high_col <- "#004D40"
        vals_vec <- met$separation
      } else if (color_mode == "density") {
        label_text <- "Fuzzy Density"
        low_col <- "#FFF3E0"
        high_col <- "#E65100"
        vals_vec <- met$density
      }
      
      min_val <- if (length(vals_vec) > 0) round(min(vals_vec, na.rm = TRUE), 3) else 0
      max_val <- if (length(vals_vec) > 0) round(max(vals_vec, na.rm = TRUE), 3) else 1
      
      div(
        class = "d-flex align-items-center gap-2",
        style = "min-width: 300px; max-width: 400px; font-weight: 500;",
        span(class = "text-muted small", min_val),
        div(
          style = sprintf(
            "height: 12px; width: 150px; border-radius: 6px; background: linear-gradient(to right, %s, %s); border: 1px solid #dddddd;",
            low_col, high_col
          )
        ),
        span(class = "text-muted small", max_val),
        span(class = "badge bg-secondary ms-2", label_text)
      )
    }
  })

  # --- RENDER INTERACTIVE PLOT ---
  output$interactivePlot <- renderVisNetwork({
    vals$trigger
    req(vals$fc)
    
    # Auto-compute concepts if not computed yet
    if (is.null(vals$fc$concepts) || vals$fc$concepts$size() == 0) {
      withProgress(message = "Computing concept lattice...", value = 0.5, {
        vals$fc$find_concepts()
      })
    }
    
    n <- vals$fc$concepts$size()
    if (n == 0) return(NULL)
    
    scale_node_size <- if (!is.null(input$scale_node_support)) input$scale_node_support else FALSE
    lbl_mode <- if (!is.null(input$labeling_mode)) input$labeling_mode else "reduced"
    col_mode <- if (!is.null(input$color_mode)) input$color_mode else "irreducibles"
    metrics_list <- tryCatch(concepts_metrics(), error = function(e) NULL)
    
    if (isTRUE(input$exploration_mode == "micro")) {
      center_id <- as.numeric(input$center_concept_id)
      if (is.null(center_id) || is.na(center_id) || center_id < 1 || center_id > n) {
        center_id <- 1
      }
      
      intents_mat <- as.matrix(vals$fc$concepts$intents())
      neigh <- get_neighborhood_ids_binary(center_id, intents_mat)
      g_data <- getMicroGraph(
        vals$fc$concepts, 
        center_id, 
        neigh, 
        vals$fc, 
        scale_support = scale_node_size, 
        labeling_mode = lbl_mode,
        color_mode = col_mode,
        metrics = metrics_list
      )
      if (!is.null(g_data$edges) && nrow(g_data$edges) > 0) {
        vals$lattice_edges_cache <- g_data$edges
      } else {
        vals$lattice_edges_cache <- NULL
      }
      g_vis <- strip_lattice_vis_tooltips(g_data)
      
      layout_val <- input$vizLayout
      vis <- visNetwork::visNetwork(nodes = g_vis$nodes, edges = g_vis$edges)
      
      if (isTRUE(layout_val == "layout_with_sugiyama")) {
        vis <- vis %>% visNetwork::visHierarchicalLayout(
          direction = "UD",
          sortMethod = "directed",
          levelSeparation = 120,
          nodeSpacing = 150
        )
      } else {
        vis <- vis %>% visNetwork::visIgraphLayout(layout = "layout_with_kk")
      }
      
      vis %>%
        visNetwork::visOptions(
          highlightNearest = list(enabled = TRUE, algorithm = "hierarchical"),
          nodesIdSelection = FALSE
        ) %>%
        visNetwork::visEdges(
          arrows = "to",
          color = list(color = "#888888"),
          smooth = list(type = "cubicBezier", roundness = 0.5)
        ) %>%
        visNetwork::visEvents(
          selectNode = "function(properties) {
            var clickedNode = properties.nodes[0];
            Shiny.setInputValue('clicked_micro_node', clickedNode, {priority: 'event'});
          }",
          hoverNode = "function(properties) {
            if (properties.node) {
              Shiny.setInputValue('lattice_hover_node', properties.node, {priority: 'event'});
            }
          }",
          hoverEdge = "function(properties) {
            if (properties.edge) {
              fcaRvizHandleLatticeEdgeHover(properties.edge, this.body.data.edges.get(properties.edge));
            }
          }",
          click = "function(properties) {
            var edgeId = (properties.edges && properties.edges.length > 0)
              ? properties.edges[0]
              : this.getEdgeAt({x: properties.pointer.DOM.x, y: properties.pointer.DOM.y});
            if (edgeId) {
              fcaRvizHandleLatticeEdgeClick(this, edgeId, this.body.data.edges.get(edgeId));
            } else if (properties.nodes && properties.nodes.length > 0) {
              var nodeId = properties.nodes[0];
              Shiny.setInputValue('lattice_click_node', {id: nodeId, t: Date.now()}, {priority: 'event'});
              Shiny.setInputValue('clicked_micro_node', nodeId, {priority: 'event'});
            }
          }"
        ) %>%
        visNetwork::visInteraction(
          navigationButtons = FALSE,
          zoomView = TRUE,
          dragView = TRUE,
          hover = TRUE,
          tooltipDelay = 999999
        )
      
    } else {
      # Macro view
      curr_ids <- current_filtered_ids()
      n_filtered <- if (is.null(curr_ids)) n else length(curr_ids)
      thresh <- 800
      if (!is.null(input$enable_threshold)) {
        thresh <- if (isTRUE(input$enable_threshold)) (if (!is.null(input$safety_threshold)) input$safety_threshold else 800) else Inf
      }
      if (n_filtered > thresh && !vals$force_interactive_render) return(NULL)
      tryCatch({ 
        g <- getGraph(
          vals$fc$concepts, 
          vals$fc, 
          filter_ids = current_filtered_ids(), 
          scale_support = scale_node_size, 
          labeling_mode = lbl_mode,
          color_mode = col_mode,
          metrics = metrics_list
        )
        if (!is.null(g$edges) && nrow(g$edges) > 0) {
          vals$lattice_edges_cache <- g$edges
        } else {
          vals$lattice_edges_cache <- NULL
        }
        vals$original_nodes_df <- data.frame(
          id = as.character(g$nodes$id),
          color.background = g$nodes$color.background,
          color.border = g$nodes$color.border,
          size = if ("size" %in% names(g$nodes)) g$nodes$size else 22,
          borderWidth = if ("borderWidth" %in% names(g$nodes)) g$nodes$borderWidth else 1,
          stringsAsFactors = FALSE
        )
        g_vis <- strip_lattice_vis_tooltips(g)
        
        # Configure layout
        layout_val <- input$vizLayout
        vis <- visNetwork::visNetwork(nodes = g_vis$nodes, edges = g_vis$edges)
        
        if (isTRUE(layout_val == "layout_with_sugiyama")) {
          vis <- vis %>% visNetwork::visIgraphLayout(layout = "layout_with_sugiyama")
        } else {
          vis <- vis %>% visNetwork::visIgraphLayout(layout = "layout_with_kk")
        }
        
        vis %>%
          visNetwork::visOptions(
            highlightNearest = list(enabled = TRUE, algorithm = "hierarchical", labelOnly = FALSE),
            nodesIdSelection = TRUE
          ) %>%
          visNetwork::visEdges(
            arrows = list("to" = list(enabled = TRUE, scaleFactor = 0.5)),
            color = list(color = "#AAAAAA"),
            smooth = list(type = "cubicBezier", roundness = 0.5)
          ) %>%
          visNetwork::visEvents(
            selectNode = "function(properties) {
              var clickedNode = properties.nodes[0];
              Shiny.setInputValue('clicked_macro_node', clickedNode, {priority: 'event'});
            }",
            deselectNode = "function(properties) {
              Shiny.setInputValue('clicked_macro_node', null, {priority: 'event'});
              Shiny.setInputValue('interactivePlot_selected', '', {priority: 'event'});
            }",
            hoverNode = "function(properties) {
              if (properties.node) {
                Shiny.setInputValue('lattice_hover_node', properties.node, {priority: 'event'});
              }
            }",
            hoverEdge = "function(properties) {
              if (properties.edge) {
                fcaRvizHandleLatticeEdgeHover(properties.edge, this.body.data.edges.get(properties.edge));
              }
            }",
            click = "function(properties) {
              var edgeId = (properties.edges && properties.edges.length > 0)
                ? properties.edges[0]
                : this.getEdgeAt({x: properties.pointer.DOM.x, y: properties.pointer.DOM.y});
              if (edgeId) {
                fcaRvizHandleLatticeEdgeClick(this, edgeId, this.body.data.edges.get(edgeId));
              } else if (properties.nodes && properties.nodes.length > 0) {
                var nodeId = properties.nodes[0];
                Shiny.setInputValue('lattice_click_node', {id: nodeId, t: Date.now()}, {priority: 'event'});
                Shiny.setInputValue('clicked_macro_node', nodeId, {priority: 'event'});
              }
            }"
          ) %>%
          visNetwork::visPhysics(stabilization = FALSE, barnesHut = list(gravitationalConstant = -2000)) %>%
          visNetwork::visInteraction(
            navigationButtons = FALSE,
            zoomView = TRUE,
            dragView = TRUE,
            hover = TRUE,
            tooltipDelay = 999999
          )
      }, error = function(e) NULL)
    }
  })

  output$staticPlot <- renderPlot({ 
    vals$trigger
    req(vals$fc)
    if(vals$fc$concepts$size() <= 5000) {
      plot_lattice_static(vals$fc$concepts, vals$fc, layout_val = input$vizLayout, filter_ids = current_filtered_ids())
    }
  })

  output$downloadStaticPlot <- downloadHandler(
    filename = function() {
      paste0("static_lattice_", format(Sys.time(), "%Y%m%d_%H%M"), ".pdf")
    },
    content = function(file) {
      req(vals$fc)
      pdf(file, width = 10, height = 8)
      tryCatch({
        plot_lattice_static(vals$fc$concepts, vals$fc, layout_val = input$vizLayout, filter_ids = current_filtered_ids())
      }, error = function(e) {
        plot(NULL, xlim = c(0, 1), ylim = c(0, 1), type = "n", xlab = "", ylab = "")
        text(0.5, 0.5, "Lattice too large or plot error.")
      })
      dev.off()
    }
  )

  # --- DETALLES DE CONCEPTOS ACTUALIZADOS ---
  output$conceptSelected <- renderUI({
    req(vals$fc, vals$active_concept_id)
    index <- vals$active_concept_id
    n <- vals$fc$concepts$size()
    req(index > 0 && index <= n)
    
    # Absolute irreducible status
    status <- tryCatch(get_concept_irreducible_status(index, vals$fc$concepts, vals$fc), error = function(e) NULL)
    
    # Calculate metrics
    met <- tryCatch(concepts_metrics(), error = function(e) NULL)
    stab_val <- if (!is.null(met)) round(met$stability[index], 4) else NA
    sep_val <- if (!is.null(met)) round(met$separation[index], 4) else NA
    dens_val <- if (!is.null(met)) round(met$density[index], 4) else NA
    
    # Renders card with metrics and irreducible status
    tagList(
      div(
        class = "card border-secondary mb-3 shadow-sm",
        div(
          class = "card-header bg-secondary text-white fw-bold d-flex justify-content-between align-items-center",
          span(sprintf("Concept details: C%s", index)),
          if (!is.null(status)) span(class = "badge bg-light text-dark", status$type)
        ),
        div(
          class = "card-body p-3",
          # Grid of metrics
          div(
            class = "row g-2 text-center mb-3",
            div(
              class = "col-4",
              div(
                class = "border rounded p-2 bg-light",
                span(class = "text-muted small d-block", "Stability"),
                strong(class = "text-primary", ifelse(is.na(stab_val), "-", stab_val))
              )
            ),
            div(
              class = "col-4",
              div(
                class = "border rounded p-2 bg-light",
                span(class = "text-muted small d-block", "Separation"),
                strong(class = "text-success", ifelse(is.na(sep_val), "-", sep_val))
              )
            ),
            div(
              class = "col-4",
              div(
                class = "border rounded p-2 bg-light",
                span(class = "text-muted small d-block", "Fuzzy Density"),
                strong(class = "text-warning", ifelse(is.na(dens_val), "-", dens_val))
              )
            )
          ),
          
          # Introduced objects and attributes if any
          if (!is.null(status) && (length(status$introduced_attributes) > 0 || length(status$introduced_objects) > 0)) {
            div(
              class = "mb-3 small",
              if (length(status$introduced_attributes) > 0) {
                div(
                  class = "mb-1",
                  strong("Introduces Attributes: "),
                  span(class = "text-muted", paste(status$introduced_attributes, collapse = ", "))
                )
              },
              if (length(status$introduced_objects) > 0) {
                div(
                  class = "mb-1",
                  strong("Introduces Objects: "),
                  span(class = "text-muted", paste(status$introduced_objects, collapse = ", "))
                )
              }
            )
          },
          
          # The table of Extent and Intent
          h6("Extent and Intent Contents", class = "border-bottom pb-1 mb-2 fw-bold"),
          tableOutput("conceptSelectedTable")
        )
      )
    )
  })

  output$associationRuleSelected <- renderUI({
    vals$active_edge_nonce
    req(vals$fc)
    if (is.null(vals$active_edge_from) || is.null(vals$active_edge_to)) {
      return(div(
        class = "text-muted small p-2",
        icon("info-circle"),
        " Click an edge (arrow) in the lattice to see its association rule."
      ))
    }
    rule <- get_association_rule_details(vals$active_edge_from, vals$active_edge_to, vals$fc)
    req(!is.null(rule))

    lhs_str <- if (length(rule$lhs) > 0) paste(rule$lhs, collapse = ", ") else "\u2205"
    rhs_str <- if (length(rule$rhs) > 0) paste(rule$rhs, collapse = ", ") else "\u2205"

    div(
      class = "card border-success mb-3 shadow-sm",
      div(
        class = "card-header bg-success text-white fw-bold",
        sprintf("Association rule: C%s \u2192 C%s", rule$from_id, rule$to_id)
      ),
      div(
        class = "card-body p-3",
        div(
          class = "mb-3",
          h6("Rule (LHS \u2192 RHS)", class = "fw-bold border-bottom pb-1 mb-2"),
          div(
            class = "small",
            span(class = "badge bg-light text-dark border me-1", "LHS"),
            span("{", lhs_str, "}"),
            span(class = "mx-2", "\u2192"),
            span(class = "badge bg-light text-dark border me-1", "RHS"),
            span("{", rhs_str, "}")
          )
        ),
        div(
          class = "row g-2 text-center",
          div(
            class = "col-4",
            div(
              class = "border rounded p-2 bg-light",
              span(class = "text-muted small d-block", "Confidence"),
              strong(class = "text-success", sprintf("%.1f%%", rule$confidence * 100))
            )
          ),
          div(
            class = "col-4",
            div(
              class = "border rounded p-2 bg-light",
              span(class = "text-muted small d-block", "Parent support"),
              strong(sprintf("%.1f%%", rule$parent_support_pct)),
              span(class = "text-muted small d-block", paste0("(", rule$parent_support_n, " objs)"))
            )
          ),
          div(
            class = "col-4",
            div(
              class = "border rounded p-2 bg-light",
              span(class = "text-muted small d-block", "Child support"),
              strong(sprintf("%.1f%%", rule$child_support_pct)),
              span(class = "text-muted small d-block", paste0("(", rule$child_support_n, " objs)"))
            )
          )
        ),
        div(
          class = "text-muted small mt-3 mb-0",
          icon("mouse-pointer"),
          " Click a concept node to return to concept details."
        )
      )
    )
  })

  output$latticePropertiesUI <- renderUI({
    vals$trigger
    req(vals$fc)
    ready <- tryCatch(!vals$fc$concepts$is_empty(), error = function(e) FALSE)
    if (!ready) return(p(class="text-muted small", "No concept lattice loaded yet."))
    
    cl <- vals$fc$concepts
    
    # Calculate properties with tryCatch
    is_dist <- tryCatch(cl$is_distributive(), error = function(e) NA)
    is_mod <- tryCatch(cl$is_modular(), error = function(e) NA)
    is_semi <- tryCatch(cl$is_semimodular(), error = function(e) NA)
    is_atom <- tryCatch(cl$is_atomic(), error = function(e) NA)
    w_val <- tryCatch(cl$width(), error = function(e) NA)
    d_val <- tryCatch(cl$dimension(), error = function(e) NA)
    
    make_badge <- function(val) {
      if (is.na(val)) return(span(class="badge bg-secondary", "Unknown"))
      if (isTRUE(val)) {
        span(class="badge bg-success", "Yes")
      } else {
        span(class="badge bg-danger", "No")
      }
    }
    
    tagList(
      div(
        class = "p-2",
        div(
          class = "d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom",
          span(class="text-muted small", "Algebraic and structural metrics"),
          actionLink("btnLatticeInfo", "", icon = icon("info-circle"), style = "cursor: pointer; font-size: 1.1em; color: #0d6efd;", title = "Show definitions & scientific references")
        ),
        tags$table(
          class = "table table-sm table-borderless small mb-0",
          tags$tbody(
            tags$tr(tags$td(strong("Number of Concepts:")), tags$td(class="text-end", cl$size())),
            tags$tr(tags$td(strong("Distributive:")), tags$td(class="text-end", make_badge(is_dist))),
            tags$tr(tags$td(strong("Modular:")), tags$td(class="text-end", make_badge(is_mod))),
            tags$tr(tags$td(strong("Semimodular:")), tags$td(class="text-end", make_badge(is_semi))),
            tags$tr(tags$td(strong("Atomic:")), tags$td(class="text-end", make_badge(is_atom))),
            tags$tr(tags$td(strong("Lattice Width:")), tags$td(class="text-end", ifelse(is.na(w_val), "-", w_val))),
            tags$tr(tags$td(strong("Lattice Dimension:")), tags$td(class="text-end", ifelse(is.na(d_val), "-", d_val)))
          )
        )
      )
    )
  })

  # --- OBSERVADOR PARA EL MODAL INFORMATIVO ---
  observeEvent(input$btnLatticeInfo, {
    showModal(modalDialog(
      title = "FCA Metrics & Properties Reference Guide",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">Concept Quality Metrics</h5>
          
          <p><strong>1. Stability</strong></p>
          <ul>
            <li><strong>Definition:</strong> Measures the robustness of a formal concept. It represents the probability that the concept's intent remains closed if some objects are randomly removed from the formal context.</li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Kuznetsov, S. O. (1990). <em>On stability of a formal concept</em>. Rostov-on-Don (in Russian).</li>
                <li>Kuznetsov, S. O. (2007). <em>Concepts' Stability</em>. In: Priss, U., Pollandt, S., Ernst, G. (eds) Conceptual Structures: Knowledge Architectures for Smart Applications. Lecture Notes in Computer Science, vol 4604. Springer, Berlin.</li>
              </ul>
            </li>
          </ul>

          <p><strong>2. Separation</strong></p>
          <ul>
            <li><strong>Definition:</strong> Measures the degree to which the concept's intent isolates the objects in its extent from other objects in the context. High separation indicates a well-defined, independent conceptual cluster.</li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Klimushkin, M., Obiedkov, S., & Roth, C. (2010). <em>Faceted taxonomy construction with formal concept analysis</em>. In: Kryszkiewicz, M., Obiedkov, S. (eds) Formal Concept Analysis. ICFCA 2010. Lecture Notes in Computer Science, vol 5986. Springer, Heidelberg.</li>
              </ul>
            </li>
          </ul>

          <p><strong>3. Fuzzy Density</strong></p>
          <ul>
            <li><strong>Definition:</strong> The proportion of cross cells (or membership values in fuzzy contexts) inside the submatrix defined by the extent and intent of the concept, relative to the total possible cells in that subcontext.</li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Belohlavek, R. (1999). <em>Fuzzy Galois connections</em>. Mathematical Logic Quarterly, 45(4), 497-504.</li>
                <li>Belohlavek, R. (2002). <em>Fuzzy Relational Systems: Foundations and Principles</em>. Kluwer Academic Publishers.</li>
              </ul>
            </li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Structural and Algebraic Properties of the Lattice</h5>

          <p><strong>4. Distributivity, Modularity and Semimodularity</strong></p>
          <ul>
            <li><strong>Definitions:</strong> 
              <ul>
                <li><em>Distributive Lattice:</em> A complete lattice where meet and join operations distribute over each other. It contains no sublattices isomorphic to the pentagon N<sub>5</sub> or the diamond M<sub>5</sub>.</li>
                <li><em>Modular Lattice:</em> Satisfies the modular identity: if x &le; z, then x v (y ^ z) = (x v y) ^ z.</li>
                <li><em>Semimodular Lattice:</em> If x and y cover their meet x ^ y, then their join x v y covers both x and y.</li>
              </ul>
            </li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Birkhoff, G. (1940). <em>Lattice Theory</em>. American Mathematical Society Colloquium Publications, vol. 25.</li>
                <li>Ganter, B., & Wille, R. (1999). <em>Formal Concept Analysis: Mathematical Foundations</em>. Springer-Verlag.</li>
              </ul>
            </li>
          </ul>

          <p><strong>5. Lattice Width</strong></p>
          <ul>
            <li><strong>Definition:</strong> The maximum size of an antichain; that is, the largest set of pairwise incomparable concepts. It measures the maximum degree of divergence or parallelism of the taxonomy.</li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Dilworth, R. P. (1950). <em>A Decomposition Theorem for Partially Ordered Sets</em>. Annals of Mathematics, 51(1), 161-166.</li>
              </ul>
            </li>
          </ul>

          <p><strong>6. Lattice Dimension</strong></p>
          <ul>
            <li><strong>Definition:</strong> The Dushnik-Miller dimension. It represents the minimum number of linear orders (chains) whose intersection yields exactly the lattice order relation. Graphically, it indicates the minimum number of independent coordinates needed to position the concepts.</li>
            <li><strong>Scientific Reference:</strong> 
              <ul>
                <li>Dushnik, B., & Miller, E. W. (1941). <em>Partially ordered sets</em>. American Journal of Mathematics, 63(3), 600-610.</li>
              </ul>
            </li>
          </ul>
        </div>
      ")
    ))
  })

  # --- OBSERVADOR PARA INFORMACIÓN GENERAL DE FCA ---
  observeEvent(input$btnFcaInfo, {
    showModal(modalDialog(
      title = "Introduction to Formal Concept Analysis (FCA)",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">What is Formal Concept Analysis (FCA)?</h5>
          <p>Formal Concept Analysis is a mathematical data analysis discipline that identifies conceptual structures from binary relations between objects and attributes.</p>
          
          <p><strong>The Formal Context:</strong></p>
          <p>It is mathematically defined as a triple K = (G, M, I) where:</p>
          <ul>
            <li><strong>G (Objects):</strong> A set of objects (Gegenstände).</li>
            <li><strong>M (Attributes):</strong> A set of attributes (Merkmale).</li>
            <li><strong>I (Incidence):</strong> A binary relation between them (I &sube; G &times; M). If (g, m) &in; I, it indicates that object <em>g</em> has attribute <em>m</em>.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Primary Reference of FCA</h5>
          <p>The foundational formulation of FCA was described in depth in the classic book:</p>
          <div class=\"p-3 bg-light border rounded mb-3\">
            <strong>Ganter, B., & Wille, R. (1999).</strong> <em>Formal Concept Analysis: Mathematical Foundations</em>. Springer-Verlag.
          </div>
          <p>This work sets the foundations of Galois connection, derivation operators, and the fundamental theorem of concept lattices.</p>
        </div>
      ")
    ))
  })

  # --- OBSERVADOR PARA OPERACIONES BÁSICAS Y REDUCCIÓN ---
  observeEvent(input$btnBasicOpsInfo, {
    showModal(modalDialog(
      title = "Set Operations & Context Simplification",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">Derivation Operators</h5>
          <p>In FCA, the Galois connection is materialized via Intent and Extent operators (typically denoted by a prime symbol '):</p>
          <ul>
            <li><strong>Intent:</strong> Given a set of objects A, A' is the set of all attributes shared by all objects in A.</li>
            <li><strong>Extent:</strong> Given a set of attributes B, B' is the set of all objects that possess all attributes in B.</li>
            <li><strong>Closure:</strong> The composite operator (A'') is a closure operator. A set is closed if A = A''.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Context Simplification & Reduction</h5>
          <ul>
            <li><strong>Clarify:</strong> Groups objects or attributes with identical incidence patterns in the context (i.e. equivalent elements), leaving a single representative.</li>
            <li><strong>Reduce:</strong> Removes redundant attributes or objects (those that are clarify-able or expressible as the intersection/closure of others), simplifying the context without losing conceptual information.</li>
            <li><strong>Arrow Relations:</strong> Structured relations (&swarrow;, &nearrow;, &updownarrow;) in the context that efficiently characterize the irreducibility of attributes and objects for their removal.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Bibliographical References</h5>
          <div class=\"p-3 bg-light border rounded mb-3\">
            <ul>
              <li><strong>Ganter, B., & Wille, R. (1999).</strong> <em>Formal Concept Analysis: Mathematical Foundations</em>. Springer-Verlag.</li>
              <li><strong>Wille, R. (1982).</strong> <em>Restructuring lattice theory: an approach based on hierarchies of concepts</em>. Ordered Sets, Reidel, Dordrecht, 445-470.</li>
            </ul>
          </div>
        </div>
      ")
    ))
  })

  # --- OBSERVADOR PARA ALGORITMOS DE CÁLCULO DEL RETÍCULO ---
  observeEvent(input$btnLatticeAlgoInfo, {
    showModal(modalDialog(
      title = "Concept Lattice Construction Algorithms",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">Computing the Concept Lattice</h5>
          <p>A formal concept is a pair (A, B) such that A' = B and B' = A. The concept lattice is the set of all concepts ordered hierarchically by extent inclusion (or reverse intent inclusion).</p>
          
          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Implemented Algorithms</h5>
          <p>The <code>fcaR</code> package implements efficient state-of-the-art algorithms to find all formal concepts:</p>
          <ul>
            <li><strong>NextClosure:</strong> Ganter's foundational lexicographical traversal algorithm. Sequential and deterministic, with high theoretical value.</li>
            <li><strong>InClose (Close-by-One / In-Close):</strong> A modern, highly efficient algorithm developed by Stephen Andrews, which optimizes computations using local checks and avoiding candidate repetition via attribute tree traversal.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Bibliographical References</h5>
          <div class=\"p-3 bg-light border rounded mb-3\">
            <ul>
              <li><strong>Ganter, B. (1984).</strong> <em>Two basic algorithms in formal concept analysis</em>. Technische Hochschule Darmstadt, Preprint 831.</li>
              <li><strong>Andrews, S. (2009).</strong> <em>In-Close, a Fast Algorithm for Computing Formal Concepts</em>. In: Conceptual Structures: Leveraging Semantic Technologies. ICCS 2009. Lecture Notes in Computer Science, vol 5662. Springer.</li>
              <li><strong>Kuznetsov, S. O. (1999).</strong> <em>Learning of simple conceptual graphs from positive and negative examples</em>. Principles of Data Mining and Knowledge Discovery, PKDD '99.</li>
            </ul>
          </div>
        </div>
      ")
    ))
  })

  # --- OBSERVADOR PARA ALGORITMOS DE IMPLICACIONES Y LÓGICA DE SIMPLIFICACIÓN ---
  observeEvent(input$btnImplicationsInfo, {
    showModal(modalDialog(
      title = "Implications & Simplification Logic",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">Implication Mining (LinCbO)</h5>
          <p>An implication A &rarr; B represents a rule stating that any object that possesses the set of attributes A will also possess the set B. In FCA, we aim to compute a minimal informative basis (typically the Duquenne-Guigues basis) to capture all such rules.</p>
          <ul>
            <li><strong>LinCbO (Linear Close-by-One):</strong> An optimized implication basis extraction algorithm used by <code>fcaR</code> to compute rules linearly through a combined search of closures and lexicographical order.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Simplification Logic</h5>
          <p>Simplification Logic is an alternative logical system to Armstrong's Axioms for reasoning with implications, designed specifically for automated computational processing via logical equivalence rules:</p>
          <ul>
            <li>Allows drastic reduction of the size and attribute count of redundant implications.</li>
            <li>Key simplification rules: premise reduction, composition, and redundant attribute elimination via closures.</li>
          </ul>

          <h5 class=\"text-primary border-bottom pb-2 mt-4\">Bibliographical References</h5>
          <div class=\"p-3 bg-light border rounded mb-3\">
            <ul>
              <li><strong>Obiedkov, S., & Duquenne, V. (2007).</strong> <em>Attribute exploration with background knowledge</em>. Annals of Mathematics and Artificial Intelligence, 49(1-4), 201-219.</li>
              <li><strong>Guigues, J.-L., & Duquenne, V. (1986).</strong> <em>Familles minimales d'implications informatives dans un tableau de données binaires</em>. Mathématiques et Sciences Humaines, 95, 5-18.</li>
              <li><strong>Cordero, P., Enciso, M., Mora, A., & Pérez de Guzmán, I. (2002).</strong> <em>Simplification logic for S-implications</em>. Logic Journal of the IGPL, 10(4), 361-381.</li>
              <li><strong>Mora, A., Cordero, P., Enciso, M., Fortes, I., & Guzmán, I. (2012).</strong> <em>Closure via simplification logic</em>. International Journal of Computer Mathematics, 89(3), 380-399.</li>
            </ul>
          </div>
        </div>
      ")
    ))
  })

  output$conceptSelectedTable <- renderTable({ index <- vals$active_concept_id; req(index > 0 && index <= vals$fc$concepts$size()); get_concepts_dataframe(vals$fc$concepts[index], vals$fc) })
  output$upperNeighbours <- renderUI({
    req(vals$fc, vals$active_concept_id)
    index <- vals$active_concept_id
    req(index > 0 && index <= vals$fc$concepts$size())
    upper <- vals$fc$concepts$upper_neighbours(vals$fc$concepts[index])
    if (upper$size() == 0) {
      return(div(class = "text-muted small p-2", icon("info-circle"), " No upper neighbours (this is a top concept)."))
    }
    tableOutput("upperNeighboursTable")
  })
  output$upperNeighboursTable <- renderTable({
    index <- vals$active_concept_id
    req(index > 0 && index <= vals$fc$concepts$size())
    get_concepts_dataframe(vals$fc$concepts$upper_neighbours(vals$fc$concepts[index]), vals$fc)
  })
  output$lowerNeighbours <- renderUI({
    req(vals$fc, vals$active_concept_id)
    index <- vals$active_concept_id
    req(index > 0 && index <= vals$fc$concepts$size())
    lower <- vals$fc$concepts$lower_neighbours(vals$fc$concepts[index])
    if (lower$size() == 0) {
      return(div(class = "text-muted small p-2", icon("info-circle"), " No lower neighbours (this is a bottom concept)."))
    }
    tableOutput("lowerNeighboursTable")
  })
  output$lowerNeighboursTable <- renderTable({
    index <- vals$active_concept_id
    req(index > 0 && index <= vals$fc$concepts$size())
    get_concepts_dataframe(vals$fc$concepts$lower_neighbours(vals$fc$concepts[index]), vals$fc)
  })

  # --- TAB LATTICE FILTERING LOGIC ---
  vals$tab_concept_filter_ids <- NULL

  observeEvent(input$btnApplyTabFilters, {
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) {
      shinyalert("Oops!", "Please compute concepts first.", type = "warning")
      return()
    }
    filtered <- calculate_filtered_concept_ids(vals$fc, input$filterObjsTab, input$filterAttsTab)
    vals$tab_concept_filter_ids <- filtered
    vals$concept_filter_ids <- filtered
    
    # Synchronize selectize inputs in the sidebar
    updateSelectizeInput(session, "filterObjs", choices = vals$fc$objects, selected = input$filterObjsTab)
    updateSelectizeInput(session, "filterAtts", choices = vals$fc$attributes, selected = input$filterAttsTab)
  })

  observeEvent(input$btnClearTabFilters, {
    req(vals$fc)
    updateSelectizeInput(session, "filterObjsTab", choices = vals$fc$objects, selected = character(0))
    updateSelectizeInput(session, "filterAttsTab", choices = vals$fc$attributes, selected = character(0))
    updateSelectizeInput(session, "filterObjs", choices = vals$fc$objects, selected = character(0))
    updateSelectizeInput(session, "filterAtts", choices = vals$fc$attributes, selected = character(0))
    vals$tab_concept_filter_ids <- NULL
    vals$concept_filter_ids <- NULL
  })

  output$tabFilterStatusBadge <- renderText({
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) return("")
    n_total <- vals$fc$concepts$size()
    if (is.null(vals$tab_concept_filter_ids)) {
      return(paste("Showing all", n_total, "concepts"))
    } else {
      n_filtered <- length(vals$tab_concept_filter_ids)
      return(paste("Showing", n_filtered, "of", n_total, "concepts"))
    }
  })

  output$tabFilteredTable <- renderTable({
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) return(NULL)
    
    n_total <- vals$fc$concepts$size()
    ids <- if (is.null(vals$tab_concept_filter_ids)) 1:n_total else vals$tab_concept_filter_ids
    
    if (length(ids) == 0) return(data.frame(Message = "No matching concepts found."))
    
    get_concepts_dataframe(vals$fc$concepts[ids], vals$fc)
  })

  observeEvent(input$btnFilterHelp, {
    showModal(modalDialog(
      title = "Concept Lattice Filtering Guide",
      size = "m",
      easyClose = TRUE,
      footer = modalButton("Close"),
      HTML("
        <div style=\"font-family: 'Inter', sans-serif; line-height: 1.6;\">
          <h5 class=\"text-primary border-bottom pb-2\">How does the Lattice Filter work?</h5>
          <p>Filtering allows you to focus on specific sub-regions of the concept lattice by restricting the concepts shown in the graph and table.</p>
          <ul>
            <li><strong>Filter by Objects:</strong> Retains only the concepts whose <strong>Extent</strong> contains ALL the selected objects.</li>
            <li><strong>Filter by Attributes:</strong> Retains only the concepts whose <strong>Intent</strong> contains ALL the selected attributes.</li>
          </ul>
          <p>When you apply the filter, the graph displays the corresponding <strong>sub-poset</strong>, drawing the hierarchical connections (meet/join lines) between the selected concepts only. The concepts table shows their full extents, intents, and algebraic classifications.</p>
        </div>
      ")
    ))
  })

  # --- TAB LATTICE ASSOCIATION RULES LOGIC ---
  observeEvent(input$btnComputeAssocRules, {
    req(vals$fc)
    if (tryCatch(vals$fc$concepts$is_empty(), error = function(e) TRUE)) {
      shinyalert("Oops!", "Please compute concepts first.", type = "warning")
      return()
    }
    
    selected_cid <- NULL
    if (isTRUE(input$assoc_use_selected_concept)) {
      selected_cid <- vals$active_concept_id
      if (is.null(selected_cid) || selected_cid == 0) {
        shinyalert("No Concept Selected", "Please select a concept in the Interactive Explorer first.", type = "warning")
        return()
      }
    }
    
    withProgress(message = "Extracting association rules from lattice...", value = 0.5, {
      tryCatch({
        df <- get_association_rules(
          fc = vals$fc,
          min_support = input$assoc_support,
          min_confidence = input$assoc_confidence,
          selected_concept_id = selected_cid,
          method = if (!is.null(input$assoc_method)) input$assoc_method else "covering"
        )
        vals$assoc_rules_df <- df
        
        n_rules <- if (is.null(df)) 0 else nrow(df)
        if (n_rules > 0) {
          shinyalert("Success", sprintf("Successfully extracted %d association rules from the concept lattice.", n_rules), type = "success")
        } else {
          shinyalert("No Rules", "No association rules met the specified support and confidence thresholds.", type = "warning")
        }
      }, error = function(e) {
        shinyalert("Extraction Error", e$message, type = "error")
      })
    })
  })

  observe({
    req(vals$fc)
    req(input$assoc_use_selected_concept)
    req(vals$active_concept_id)
    
    selected_cid <- vals$active_concept_id
    if (selected_cid > 0 && selected_cid <= vals$fc$concepts$size()) {
      tryCatch({
        df <- get_association_rules(
          fc = vals$fc,
          min_support = input$assoc_support,
          min_confidence = input$assoc_confidence,
          selected_concept_id = selected_cid,
          method = if (!is.null(input$assoc_method)) input$assoc_method else "covering"
        )
        vals$assoc_rules_df <- df
      }, error = function(e) NULL)
    }
  })

  output$assocRulesStatusBadge <- renderText({
    req(vals$fc)
    if (is.null(vals$assoc_rules_df)) {
      return("Extract rules to view results.")
    }
    n_rules <- nrow(vals$assoc_rules_df)
    return(sprintf("Extracted %d association rules", n_rules))
  })

  output$assocRulesTable <- DT::renderDT({
    req(vals$assoc_rules_df)
    df <- vals$assoc_rules_df
    if (nrow(df) == 0) {
      return(DT::datatable(data.frame(Message = "No rules extracted."), options = list(dom = "t")))
    }
    
    df_disp <- df
    df_disp$Support <- sprintf("%.1f%%", df_disp$Support * 100)
    df_disp$Confidence <- sprintf("%.1f%%", df_disp$Confidence * 100)
    
    colnames(df_disp) <- c("LHS (Antecedent)", "RHS (Consequent)", "Support", "Confidence", "LHS Concept ID", "RHS Concept ID")
    
    DT::datatable(
      df_disp,
      options = list(
        pageLength = 10,
        lengthMenu = c(10, 25, 50, 100),
        searchDelay = 500,
        dom = "lfrtip"
      ),
      selection = "none",
      rownames = FALSE
    )
  })

  output$downloadAssocRulesRds <- downloadHandler(
    filename = function() {
      paste0("lattice_association_rules_", format(Sys.time(), "%Y%m%d_%H%M"), ".rds")
    },
    content = function(file) {
      req(vals$assoc_rules_df)
      saveRDS(vals$assoc_rules_df, file)
    }
  )

  # --- SEMANTIC JOURNEY TIMELINE ---
  output$semanticJourneyTimeline <- renderUI({
    req(vals$fc, vals$navigation_history)
    history_ids <- vals$navigation_history
    if (length(history_ids) == 0) {
      return(p(class="text-muted text-center py-3", "Your semantic journey is empty. Visit concepts to start your timeline!"))
    }
    
    intents_list <- vals$fc$concepts$intents()
    att_names <- clean_latex_names(vals$fc$attributes)
    
    timeline_items <- list()
    
    # Let's add a clear button at the top
    timeline_items[[1]] <- div(
      class = "d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom",
      span(class="fw-bold text-secondary", paste("Steps visited:", length(history_ids))),
      actionButton("btnClearHistory", "Clear Journey", class="btn-xs btn-outline-danger", icon = icon("trash-can"))
    )
    
    for (i in 1:length(history_ids)) {
      curr_id <- history_ids[i]
      
      curr_intent_idx <- which(as.matrix(intents_list[, curr_id]) > 0)
      curr_intent_names <- att_names[curr_intent_idx]
      curr_intent_str <- if(length(curr_intent_names) > 0) paste(curr_intent_names, collapse = ", ") else "{}"
      
      added_str <- ""
      removed_str <- ""
      
      if (i > 1) {
        prev_id <- history_ids[i - 1]
        prev_intent_idx <- which(as.matrix(intents_list[, prev_id]) > 0)
        prev_intent_names <- att_names[prev_intent_idx]
        
        added <- setdiff(curr_intent_names, prev_intent_names)
        removed <- setdiff(prev_intent_names, curr_intent_names)
        
        if (length(added) > 0) {
          added_str <- div(
            class = "mt-1",
            span(class="badge bg-success-subtle text-success me-1", "+ Added:"),
            span(class="text-success small fw-semibold", paste(added, collapse = ", "))
          )
        }
        if (length(removed) > 0) {
          removed_str <- div(
            class = "mt-1",
            span(class="badge bg-danger-subtle text-danger me-1", "- Relaxed:"),
            span(class="text-danger small fw-semibold", paste(removed, collapse = ", "))
          )
        }
      }
      
      isActive <- (curr_id == vals$active_concept_id)
      card_class <- if (isActive) "border-primary bg-primary-subtle shadow-sm" else "border-light"
      
      item_ui <- div(
        class = paste("card mb-2", card_class),
        style = "transition: all 0.2s ease;",
        div(
          class = "card-body p-2",
          div(
            class = "d-flex justify-content-between align-items-center",
            span(class="fw-bold", paste("Step", i, ": Concept", curr_id)),
            tags$a(
              href = "#",
              class = "btn btn-link btn-sm p-0 text-decoration-none",
              onclick = sprintf("Shiny.setInputValue('jump_concept_id', %d, {priority: 'event'}); return false;", curr_id),
              icon("circle-chevron-right"),
              " Jump"
            )
          ),
          div(class="text-muted small mt-1", paste("Profile:", curr_intent_str)),
          added_str,
          removed_str
        )
      )
      
      timeline_items[[i + 1]] <- item_ui
    }
    
    n_items <- length(timeline_items)
    if (n_items > 2) {
      timeline_items[2:n_items] <- rev(timeline_items[2:n_items])
    }
    
    div(style = "max-height: 450px; overflow-y: auto; padding-right: 5px;", timeline_items)
  })

  output$downloadRdsConp <- downloadHandler(filename = function() { "concepts.rds" }, content = function(file) { saveRDS(vals$fc$concepts, file) })

  # --- ADVANCED EXPORT DOWNLOAD HANDLERS ---
  output$downloadSVG <- downloadHandler(
    filename = function() {
      paste0("concept_lattice_", format(Sys.time(), "%Y%m%d_%H%M"), ".svg")
    },
    content = function(file) {
      req(vals$fc)
      # Setup margins and plotting device
      svg(file, width = 10, height = 8)
      op <- par(mar = c(1, 1, 3, 1))
      on.exit({
        par(op)
        dev.off()
      })
      
      tryCatch({
        plot_lattice_static(
          concepts = vals$fc$concepts, 
          fc = vals$fc, 
          layout_val = input$vizLayout,
          filter_ids = current_filtered_ids()
        )
      }, error = function(e) {
        plot(NULL, xlim = c(0, 1), ylim = c(0, 1), type = "n", xlab = "", ylab = "")
        text(0.5, 0.5, paste("Error rendering SVG:", e$message))
      })
    }
  )

  output$downloadPDF <- downloadHandler(
    filename = function() {
      paste0("concept_lattice_", format(Sys.time(), "%Y%m%d_%H%M"), ".pdf")
    },
    content = function(file) {
      req(vals$fc)
      pdf(file, width = 10, height = 8)
      op <- par(mar = c(1, 1, 3, 1))
      on.exit({
        par(op)
        dev.off()
      })
      
      tryCatch({
        plot_lattice_static(
          concepts = vals$fc$concepts, 
          fc = vals$fc, 
          layout_val = input$vizLayout,
          filter_ids = current_filtered_ids()
        )
      }, error = function(e) {
        plot(NULL, xlim = c(0, 1), ylim = c(0, 1), type = "n", xlab = "", ylab = "")
        text(0.5, 0.5, paste("Error rendering PDF:", e$message))
      })
    }
  )

  output$downloadDOT <- downloadHandler(
    filename = function() {
      paste0("concept_lattice_", format(Sys.time(), "%Y%m%d_%H%M"), ".dot")
    },
    content = function(file) {
      req(vals$fc)
      tryCatch({
        dot_code <- generate_dot_code(vals$fc$concepts, vals$fc, filter_ids = current_filtered_ids())
        writeLines(dot_code, file)
      }, error = function(e) {
        writeLines(paste("// Error generating DOT code:", e$message), file)
      })
    }
  )

  output$downloadGraphML <- downloadHandler(
    filename = function() {
      paste0("concept_lattice_", format(Sys.time(), "%Y%m%d_%H%M"), ".graphml")
    },
    content = function(file) {
      req(vals$fc)
      tryCatch({
        scale_node_size <- if (!is.null(input$scale_node_support)) input$scale_node_support else FALSE
        lbl_mode <- if (!is.null(input$labeling_mode)) input$labeling_mode else "reduced"
        g_data <- getGraph(
          vals$fc$concepts, 
          vals$fc, 
          filter_ids = current_filtered_ids(),
          scale_support = scale_node_size,
          labeling_mode = lbl_mode
        )
        nodes <- g_data$nodes
        edges <- g_data$edges
        
        # Create igraph
        if (nrow(edges) > 0) {
          g_igraph <- igraph::graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
        } else {
          g_igraph <- igraph::make_empty_graph(n = nrow(nodes))
          igraph::V(g_igraph)$name <- nodes$id
        }
        
        # Add attributes to vertices for Gephi to recognize
        igraph::V(g_igraph)$label <- nodes$label
        igraph::V(g_igraph)$concept_id <- nodes$id
        igraph::V(g_igraph)$color <- nodes$color.background
        
        # Write to file
        igraph::write_graph(g_igraph, file, format = "graphml")
      }, error = function(e) {
        writeLines(paste0("<!-- Error generating GraphML: ", e$message, " -->"), file)
      })
    }
  )

  observeEvent(input$createLatexConcepts, {
    req(vals$fc)
    showModal(modalDialog(
      title = "LaTeX Premium Export",
      size = "l",
      easyClose = TRUE,
      tabsetPanel(
        tabPanel("Concept Lattice (TikZ)",
                 p(class="text-muted small mt-2", "This generates native TikZ graphical code for the currently active concept lattice (respecting your current filters)."),
                 div(style="position:relative;",
                     verbatimTextOutput("latexTikzCodeText"),
                     div(class="text-end mt-2",
                         downloadButton("downloadTikzFile", "Download .tex", class="btn-primary btn-sm")
                     )
                 )
        ),
        tabPanel("Formal Context (Tabular)",
                 p(class="text-muted small mt-2", "This generates a clean tabular LaTeX code representing the formal context incidence matrix."),
                 div(style="position:relative;",
                     verbatimTextOutput("latexTabularCodeText"),
                     div(class="text-end mt-2",
                         downloadButton("downloadTabularFile", "Download .tex", class="btn-primary btn-sm")
                     )
                 )
        ),
        tabPanel("Raw Concept List",
                 p(class="text-muted small mt-2", "The standard fcaR representation of concepts in LaTeX format."),
                 div(style="position:relative;",
                     verbatimTextOutput("latexRawConceptsText"),
                     div(class="text-end mt-2",
                         downloadButton("downloadRawConceptsFile", "Download .tex", class="btn-primary btn-sm")
                     )
                 )
        )
      ),
      footer = modalButton("Close")
    ))
  })

  # Renderers for the LaTeX Modal
  output$latexTikzCodeText <- renderText({
    req(vals$fc)
    lattice_to_tikz(concepts = vals$fc$concepts, fc = vals$fc, layout_val = input$vizLayout, filter_ids = current_filtered_ids())
  })

  output$latexTabularCodeText <- renderText({
    req(vals$fc)
    context_to_latex_tabular(vals$fc)
  })

  output$latexRawConceptsText <- renderText({
    req(vals$fc)
    vals$fc$concepts$to_latex()
  })

  # Download Handlers for the LaTeX snippets
  output$downloadTikzFile <- downloadHandler(
    filename = function() { "concept_lattice_tikz.tex" },
    content = function(file) {
      writeLines(lattice_to_tikz(concepts = vals$fc$concepts, fc = vals$fc, layout_val = input$vizLayout, filter_ids = current_filtered_ids()), file)
    }
  )

  output$downloadTabularFile <- downloadHandler(
    filename = function() { "formal_context_tabular.tex" },
    content = function(file) {
      writeLines(context_to_latex_tabular(vals$fc), file)
    }
  )

  output$downloadRawConceptsFile <- downloadHandler(
    filename = function() { "raw_concepts_list.tex" },
    content = function(file) {
      writeLines(vals$fc$concepts$to_latex(), file)
    }
  )

  # ===========================================================================
  # 5. IMPLICACIONES
  # ===========================================================================

  # 1. Compute Canonical Basis Option (from formal context)
  observeEvent(input$optComputeBasis, {
    req(vals$fc)
    withProgress(message = "Computing canonical basis...", value = 0.5, {
      tryCatch({
        run_canonical_basis(from_context = TRUE)
        shinyalert("Success", "Canonical basis computed successfully.", type = "success")
      }, error = function(e) {
        shinyalert("Error", e$message, type = "error")
      })
    })
  })

  # 2. Implication Mining Option (Show Modal matching arules2fcaR design)
  observeEvent(input$optMineImps, {
    req(vals$fc)
    showModal(modalDialog(
      title = "Mine Implications (arules apriori)",
      easyClose = TRUE,
      footer = tagList(
        modalButton("Cancel"),
        actionButton("btnExecuteMining", "Run Mining", class = "btn-primary", icon = icon("play"))
      ),
      fluidPage(
        p("Mine rules from the formal context using association rule algorithms. Mined rules will automatically have their confidence set to 1.0 and be converted into fcaR implications."),
        sliderInput("mineSupp", "Min Support:", min = 0.01, max = 1, value = 0.05, step = 0.01),
        layout_columns(
          col_widths = c(6, 6),
          numericInput("mineMinLen", "Min Len:", value = 1, min = 1),
          numericInput("mineMaxLen", "Max Len:", value = 10, min = 1)
        ),
        div(class="alert alert-info small p-2 mb-2", "Confidence fixed at 1.0 (Implications).")
      )
    ))
  })

  # Execute Implication Mining
  observeEvent(input$btnExecuteMining, {
    req(vals$fc)
    removeModal()
    
    showNotification("Mining implications in the background... You can continue browsing.", 
                     type = "message", id = "bg_mining_notif", duration = NULL)
    
    mat <- t(as.matrix(vals$fc$I))
    supp_val <- input$mineSupp
    minlen_val <- input$mineMinLen
    maxlen_val <- input$mineMaxLen
    
    future({
      library(arules)
      trans <- as(mat, "transactions")
      params <- list(supp = supp_val, conf = 1.0, minlen = minlen_val, maxlen = maxlen_val)
      apriori(trans, parameter = params, control = list(verbose = FALSE))
    }, seed = TRUE) %...>% (function(rules_found) {
      removeNotification("bg_mining_notif")
      n_rules <- if (is.null(rules_found)) 0L else length(rules_found)
      if (n_rules > 0) {
        vals$fc$implications <- convert_rules_to_fcaR_safe(rules_found, vals$fc)
        vals$stats_orig <- list(
          count = vals$fc$implications$cardinality(),
          size_lhs = sum(vals$fc$implications$get_LHS_matrix()),
          size_rhs = sum(vals$fc$implications$get_RHS_matrix())
        )
        vals$filtered_imps <- vals$fc$implications
        implications_df(get_implications_dataframe(vals$fc$implications))
        vals$trigger <- vals$trigger + 1
        shinyalert("Success", paste("Successfully mined", length(rules_found), "implications."), type = "success")
      } else {
        shinyalert("No Rules", "No implications satisfied the specified support threshold.", type = "warning")
      }
    }) %...!% (function(e) {
      removeNotification("bg_mining_notif")
      shinyalert("Mining Error", e$message, type = "error")
    })
    NULL
  })

  # 3. Import from File Option (Show Modal)
  observeEvent(input$optImportImplicationsFile, {
    req(vals$fc)
    showModal(modalDialog(
      title = "Import Implications from File (.rds)",
      easyClose = TRUE,
      footer = tagList(modalButton("Cancel")),
      fileInput("fileImportImps", "Select RDS File (ImplicationSet or arules rules):", accept = c(".rds"))
    ))
  })

  # Execute File Import
  observeEvent(input$fileImportImps, {
    req(input$fileImportImps, vals$fc)
    removeModal()
    tryCatch({
      obj <- readRDS(input$fileImportImps$datapath)
      
      if (inherits(obj, "ImplicationSet")) {
        vals$fc$implications <- obj
        shinyalert("Success", "Loaded ImplicationSet from RDS file.", type = "success")
      } else if (inherits(obj, "rules")) {
        vals$fc$implications <- convert_rules_to_fcaR_safe(obj, vals$fc)
        shinyalert("Success", "Loaded and converted arules rules from RDS file.", type = "success")
      } else {
        stop("Unsupported file object. File must contain an ImplicationSet (fcaR) or rules (arules).")
      }
      
      vals$stats_orig <- list(
        count = vals$fc$implications$cardinality(),
        size_lhs = sum(vals$fc$implications$get_LHS_matrix()),
        size_rhs = sum(vals$fc$implications$get_RHS_matrix())
      )
      vals$filtered_imps <- vals$fc$implications
      implications_df(get_implications_dataframe(vals$fc$implications))
      vals$trigger <- vals$trigger + 1
    }, error = function(e) {
      shinyalert("Import Error", e$message, type = "error")
    })
  })

  # 4. Attribute Exploration Option (Show modal exploration wizard)
  observeEvent(input$optAttrExploration, {
    req(vals$fc)
    showModal(modalDialog(
      title = span(icon("route", class="text-primary"), " Interactive Attribute Exploration (Ganter's Algorithm)"),
      size = "xl",
      easyClose = FALSE,
      footer = tagList(
        actionButton("btnResetExplorationModal", "Reset Exploration", class = "btn-outline-danger", icon = icon("rotate")),
        modalButton("Close")
      ),
      fluidPage(
        layout_columns(
          col_widths = c(4, 8),
          
          # --- COLUMNA IZQUIERDA: ESTADO Y CONTROL ---
          card(
            card_header("Control Panel", class="bg-primary text-white"),
            height = "100%",
            
            div(class="p-2 bg-light border rounded mb-3 text-center",
                h5("Status", class="fw-bold text-muted small uppercase mb-1"),
                uiOutput("explorationStatusBadge")
            ),
            
            # PANEL DE INICIO
            conditionalPanel(
              condition = "!output.isExplorationActive",
              p(class="text-muted small", 
                "Attribute Exploration is a systematic method based on Bernhard Ganter and Rudolf Wille's Formal Concept Analysis (1999) for interactively acquiring domain knowledge. The system will ask you about implications that hold in your current dataset, and you will confirm them or provide counterexamples."),
              actionButton("btnStartExploration", "Start Knowledge Acquisition", 
                           icon = icon("play"), class = "btn-success w-100 btn-lg")
            ),
            
            # PANEL DE ESTADÍSTICAS ACTIVAS
            conditionalPanel(
              condition = "output.isExplorationActive",
              h6("Acquired Domain Rules", class="text-primary fw-bold mt-2"),
              uiOutput("confirmedRulesCount"),
              hr(),
              h6("Refuted Implications", class="text-danger fw-bold"),
              uiOutput("refutedRulesCount"),
              hr(),
              p(class="text-muted small italic", 
                icon("info-circle"), " Confirmed rules are saved permanently. Counterexamples automatically update your formal context matrix by adding new objects.")
            )
          ),

          # --- COLUMNA DERECHA: INTERACCIÓN Y PREGUNTAS ---
          card(
            card_header("Attribute Knowledge Wizard"),
            height = "580px",
            
            # CASO 1: NO ACTIVO
            conditionalPanel(
              condition = "!output.isExplorationActive",
              div(class="text-center py-5",
                  icon("brain", class="fa-5x text-muted mb-4"),
                  h4("Ready to Acquire Knowledge", class="fw-bold"),
                  p(class="text-muted", "Click the 'Start Knowledge Acquisition' button on the left to begin the interactive exploration session.")
              )
            ),
            
            # CASO 2: ACTIVO Y PREGUNTA DISPONIBLE (NO REFUTANDO)
            conditionalPanel(
              condition = "output.isExplorationActive && !output.isExplorationComplete && !output.isRefutingActive",
              div(class="p-4 border border-primary rounded bg-light mb-4", style="min-height: 220px;",
                  h5(class="text-primary fw-bold mb-3", icon("question-circle"), " Proposed Domain Implication:"),
                  uiOutput("questionText"),
                  hr(),
                  p(class="text-muted small", "Does this implication hold universally in your domain?")
              ),
              
              layout_columns(
                col_widths = c(6, 6),
                actionButton("btnConfirmRule", "Yes, it is a Domain Law", 
                             icon = icon("check-circle"), class = "btn-success btn-lg d-block w-100 py-3"),
                actionButton("btnRefuteRule", "No, Refute with Counterexample", 
                             icon = icon("times-circle"), class = "btn-danger btn-lg d-block w-100 py-3")
              )
            ),
            
            # CASO 3: EXPLORACIÓN COMPLETADA
            conditionalPanel(
              condition = "output.isExplorationComplete",
              div(class="text-center py-5",
                  icon("circle-check", class="fa-5x text-success mb-4"),
                  h3("Exploration Complete!", class="fw-bold text-success"),
                  p(class="text-muted fs-5 px-4", 
                    "All implications in the canonical basis of your formal context have been successfully evaluated. Your domain rules are fully established!"),
                  hr(class="my-4"),
                  h5("Confirmed Rules:", class="fw-bold text-start px-4"),
                  div(style="max-height: 180px; overflow-y: auto; text-align: left; padding: 0 25px;", 
                      uiOutput("listConfirmedRules"))
              )
            ),
            
            # PANEL DE REFUTACIÓN (COUNTEREXAMPLE INLINE)
            conditionalPanel(
              condition = "output.isRefutingActive",
              div(class="p-3 border border-danger rounded bg-light mb-3",
                  h5(class="text-danger fw-bold mb-2", icon("times-circle"), " Provide Counterexample"),
                  p(class="text-muted small mb-2", 
                    "A valid counterexample must possess ALL attributes in the Premise (LHS), but MUST LACK at least one attribute in the Conclusion (RHS)."),
                  div(class="p-2 bg-white border rounded",
                      tags$strong("Rule being refuted:"), br(),
                      uiOutput("refutingRuleKeyText")
                  )
              ),
              textInput("cex_name", "Name of the new Object", value = "", width = "100%"),
              div(style = "max-height: 150px; overflow-y: auto; margin-bottom: 15px;",
                  uiOutput("cexAttributesSelectorUI")
              ),
              layout_columns(
                col_widths = c(6, 6),
                actionButton("btnCancelRefute", "Cancel", class = "btn-outline-secondary btn-lg d-block w-100"),
                actionButton("btnSaveCex", "Save Counterexample", class = "btn-danger btn-lg d-block w-100")
              )
            )
          )
        )
      )
    ))
  })

  # Handle the reset from exploration modal button
  observeEvent(input$btnResetExplorationModal, {
    shinyjs::click("btnResetExploration")
  })

  # --- IMPLICATIONS PRESENCE TRACKING & BUTTON STATE ---
  # Broadcast via shinyjs for reliable cross-tab state
  observe({
    vals$trigger
    has_imps <- if (is.null(vals$fc)) FALSE else tryCatch(vals$fc$implications$cardinality() > 0, error = function(e) FALSE)
    if (has_imps) {
      shinyjs::hide("impl_no_imps_panel")
      shinyjs::show("impl_has_imps_panel")
    } else {
      shinyjs::show("impl_no_imps_panel")
      shinyjs::hide("impl_has_imps_panel")
    }
  })

  observe({
    vals$trigger
    req(vals$fc)
    has_imps <- tryCatch(vals$fc$implications$cardinality() > 0, error = function(e) FALSE)
    if (has_imps) {
      shinyjs::enable("createLatexImplications")
      shinyjs::enable("downloadRdsImp")
    } else {
      shinyjs::disable("createLatexImplications")
      shinyjs::disable("downloadRdsImp")
    }
  })


  output$dynamicRulesUI <- renderUI({ available_rules <- tryCatch(fcaR::equivalencesRegistry$get_entry_names(), error = function(e) c("Composition", "Generalization", "Simplification", "Reduction")); checkboxGroupInput("selectRulesImplications", "Equivalence Rules:", choices = c("ALL RULES"="all", setNames(available_rules, available_rules)), selected = c("Composition"), inline = TRUE, width = "100%") })
  output$fcImplications <- renderTable({ if (isTRUE(input$ignoreFilters)) { req(vals$fc); get_implications_dataframe(vals$fc$implications) } else { req(implications_df()); implications_df() } })
  output$fcImplicationsTop <- renderTable({ req(implications_df()); implications_df() })
  observeEvent(input$btnApplyFilters, { req(vals$fc); imp <- vals$fc$implications; if(input$support > 0) { indx <- which(vals$fc$implications$support() >= input$support); imp <- vals$fc$implications[indx] }; filtered <- imp$filter(lhs = c(input$selectLHS), not_lhs = c(input$selectNotLHS), rhs = c(input$selectRHS), not_rhs = c(input$selectNotRHS)); vals$filtered_imps <- filtered; implications_df(get_implications_dataframe(filtered)) })
  observeEvent(input$btnClearFilters, {
    req(vals$fc)
    updateSelectInput(session, "selectLHS", selected = character(0))
    updateSelectInput(session, "selectNotLHS", selected = character(0))
    updateSelectInput(session, "selectRHS", selected = character(0))
    updateSelectInput(session, "selectNotRHS", selected = character(0))
    updateSliderInput(session, "support", value = 0)
    updateNumericInput(session, "minLift", value = 1.0)
    vals$filtered_imps <- vals$fc$implications
    implications_df(get_implications_dataframe(vals$fc$implications))
  })
  # --- BASE COMPUTATION TAB HANDLERS ---
  observeEvent(input$btnComputeBasis, {
    req(vals$fc)
    shinyjs::disable("btnComputeBasis")
    showNotification("Reducing to canonical basis in the background... You can continue browsing.", 
                     type = "message", id = "bg_basis_notif", duration = NULL)
    
    fc_clon <- vals$fc$clone()
    future({
      library(fcaR)
      source("global.R")
      
      has_imps <- tryCatch(fc_clon$implications$cardinality() > 0, error = function(e) FALSE)
      if (has_imps) {
        fc_clon$implications <- fc_clon$implications$to_basis()
      } else {
        safe_find_implications(fc_clon)
      }
      fc_clon
    }, seed = TRUE) %...>% (function(res_fc) {
      vals$fc <- res_fc
      sync_implications_ui()
      removeNotification("bg_basis_notif")
      shinyjs::enable("btnComputeBasis")
      shinyalert("Success", "Implication set reduced to canonical basis.", type = "success")
    }) %...!% (function(e) {
      removeNotification("bg_basis_notif")
      shinyjs::enable("btnComputeBasis")
      shinyalert("Error", paste("Failed to compute canonical basis:", e$message), type = "error")
    })
    NULL
  })

  observeEvent(input$btnComputeDirectOptimal, {
    req(vals$fc)
    if (is.null(vals$fc$implications) || vals$fc$implications$cardinality() == 0) {
      shinyalert("No Implications", "Please compute implications first.", type = "warning")
      return()
    }
    shinyjs::disable("btnComputeDirectOptimal")
    showNotification("Computing direct-optimal system in the background... You can continue browsing.", 
                     type = "message", id = "bg_direct_notif", duration = NULL)
    
    fc_clon <- vals$fc$clone()
    future({
      library(fcaR)
      fc_clon$implications <- fc_clon$implications$to_direct_optimal()
      fc_clon
    }, seed = TRUE) %...>% (function(res_fc) {
      vals$fc <- res_fc
      vals$trigger <- vals$trigger + 1
      vals$filtered_imps <- vals$fc$implications
      implications_df(get_implications_dataframe(vals$fc$implications))
      removeNotification("bg_direct_notif")
      shinyjs::enable("btnComputeDirectOptimal")
      shinyalert("Success", "Direct-optimal system computed successfully.", type = "success")
    }) %...!% (function(e) {
      removeNotification("bg_direct_notif")
      shinyjs::enable("btnComputeDirectOptimal")
      shinyalert("Error", paste("Failed to compute direct-optimal system:", e$message), type = "error")
    })
    NULL
  })

  observeEvent(input$btnApplyLogic, { req(vals$fc, input$selectRulesImplications); rules_sel <- input$selectRulesImplications; avail <- fcaR::equivalencesRegistry$get_entry_names(); to_apply <- if ("all" %in% rules_sel) avail else intersect(rules_sel, avail); withProgress(message = "Applying Rules...", value = 0.5, { if(length(to_apply) > 0){ vals$fc$implications$apply_rules(rules = to_apply, batch_size = input$batchSize, parallelize = FALSE, reorder = isTRUE(input$reorder)); vals$trigger <- vals$trigger + 1; vals$filtered_imps <- vals$fc$implications; implications_df(get_implications_dataframe(vals$fc$implications)); showNotification("Rules applied.", type = "message") } }) })
  output$logicStats <- renderUI({ vals$trigger; req(vals$fc); imp <- vals$fc$implications; tagList(h4(paste("Rules:", imp$cardinality())), p(class="text-muted", paste("Original:", if(!is.null(vals$stats_orig)) vals$stats_orig$count else imp$cardinality()))) })
  output$implicationsForClosurePreview <- renderTable({ if(isTRUE(input$ignoreFiltersClosure)) { req(vals$fc); get_implications_dataframe(vals$fc$implications) } else { req(implications_df()); implications_df() } })
  closure_result <- reactiveVal(NULL); get_imp_set_closure <- function() { if(isTRUE(input$ignoreFiltersClosure)) return(vals$fc$implications); if(!is.null(vals$filtered_imps)) return(vals$filtered_imps); return(vals$fc$implications) }
  observeEvent(input$btnComputeClosure, { req(vals$fc, input$selectClosure); S <- Set$new(vals$fc$attributes); sapply(input$selectClosure, function(x){ do.call(S$assign, setNames(list(1), x)) }); closure_result(get_imp_set_closure()$closure(S, reduce = TRUE)); nav_select("closure_results_tabs", selected = "Closure (Set)") })
  output$closureResultText <- renderText({ req(closure_result()); paste(capture.output(print(closure_result()$closure)), collapse = "\n") })
  output$closureReducedImplications <- renderTable({ req(closure_result()); get_implications_dataframe(closure_result()$implications) })
  output$latexImplicationsText <- renderText({
    req(vals$fc)
    vals$fc$implications$to_latex()
  })
  observeEvent(input$createLatexImplications, {
    req(vals$fc)
    showModal(modalDialog(
      title = "Implications LaTeX",
      size = "l",
      easyClose = TRUE,
      footer = modalButton("Close"),
      div(style = "max-height: 65vh; overflow-y: auto;", verbatimTextOutput("latexImplicationsText"))
    ))
  })
  output$downloadRdsImp <- downloadHandler(filename = function() { "implications.rds" }, content = function(file) { saveRDS(vals$fc$implications, file) })
  output$validationReportUI <- renderUI({
    div(class="text-center py-5 text-muted",
        icon("flask", class="fa-4x mb-3"),
        h5("Ready for Implication Verification"),
        p("Select LHS and RHS attributes on the left panel, and click 'Check Implication' to run the hypothesis testing report.")
    )
  })

  observeEvent(input$btnValidate, {
    req(vals$fc, input$hypLHS, input$hypRHS)
    # Correct R6 Set assignment for premise
    LHS <- Set$new(vals$fc$attributes)
    LHS$assign(attributes = input$hypLHS, values = rep(1, length(input$hypLHS)))
    
    # Compute LHS closure under implications
    res_clo <- vals$fc$implications$closure(LHS)
    clo_atts <- vals$fc$attributes[as.numeric(res_clo$closure$get_vector()) > 0]
    
    # Check if the implication holds (if RHS is a subset of LHS closure)
    holds <- all(input$hypRHS %in% clo_atts)
    # Find missing attributes in RHS
    missing_atts <- setdiff(input$hypRHS, clo_atts)
    
    # Find counterexamples in context objects
    mat <- as.matrix(vals$fc$I)
    objs <- vals$fc$objects
    cex_list <- list()
    
    if (length(objs) > 0) {
      for (o in objs) {
        # Check if o has all LHS attributes
        has_lhs <- all(mat[input$hypLHS, o, drop = FALSE] == 1)
        # Check if o lacks at least one RHS attribute
        lacks_rhs <- any(mat[input$hypRHS, o, drop = FALSE] == 0)
        
        if (has_lhs && lacks_rhs) {
          # Get which RHS attributes are lacked by this object
          lacked_rhs <- input$hypRHS[mat[input$hypRHS, o, drop = FALSE] == 0]
          cex_list[[length(cex_list) + 1]] <- data.frame(
            Object = o,
            Lacked = paste(lacked_rhs, collapse = ", "),
            stringsAsFactors = FALSE
          )
        }
      }
    }
    
    output$validationReportUI <- renderUI({
      if (holds) {
        card(
          class = "border-success shadow-sm mb-3",
          card_header(class = "bg-success-subtle text-success d-flex align-items-center",
                      icon("circle-check", class="me-2 fs-4"), 
                      span("Hypothesis Holds!", class="fw-bold fs-5")),
          card_body(
            p(class="lead text-success", "This implication rule is mathematically valid in your current system."),
            p("Any object that has the premise attributes is guaranteed to also have the conclusion attributes."),
            hr(),
            h6("Proof Details", class="fw-bold text-muted small uppercase mb-2"),
            p("The closure of your premise contains all attributes of your conclusion:"),
            div(class="p-3 bg-light border rounded",
                tags$strong("Premise (LHS): "), br(),
                lapply(input$hypLHS, function(a) span(a, class="badge bg-dark me-1")),
                br(), br(),
                tags$strong("Closure(LHS): "), br(),
                lapply(clo_atts, function(a) {
                  is_in_rhs <- a %in% input$hypRHS
                  span(a, class = paste("badge me-1", if(is_in_rhs) "bg-success" else "bg-secondary"))
                })
            )
          )
        )
      } else {
        # Implication does not hold
        cex_table <- NULL
        if (length(cex_list) > 0) {
          df_cex <- do.call(rbind, cex_list)
          cex_table <- div(
            class = "table-responsive mt-3",
            tags$table(
              class = "table table-striped table-hover table-sm border",
              tags$thead(
                tags$tr(
                  tags$th("Counterexample Object"),
                  tags$th("Lacked Conclusion Attributes")
                )
              ),
              tags$tbody(
                lapply(1:nrow(df_cex), function(row_idx) {
                  tags$tr(
                    tags$td(tags$strong(df_cex$Object[row_idx])),
                    tags$td(
                      lapply(strsplit(df_cex$Lacked[row_idx], ", ")[[1]], function(a) {
                        span(a, class="badge bg-danger me-1")
                      })
                    )
                  )
                })
              )
            )
          )
        } else {
          cex_table <- div(class="alert alert-info py-2 small mt-3", 
                           icon("info-circle"), " No counterexamples were found in the current formal context objects, but the implication fails due to the global rules of the system.")
        }
        
        card(
          class = "border-danger shadow-sm mb-3",
          card_header(class = "bg-danger-subtle text-danger d-flex align-items-center",
                      icon("circle-xmark", class="me-2 fs-4"), 
                      span("Hypothesis Fails!", class="fw-bold fs-5")),
          card_body(
            p(class="lead text-danger", "This implication rule does not hold in the current system."),
            p("There are cases where the premise holds but the conclusion does not."),
            hr(),
            h6("Failing Attributes", class="fw-bold text-muted small uppercase mb-2"),
            p("The following attributes from the conclusion are not implied by the premise closure:"),
            div(class="p-2 bg-light border rounded mb-3",
                lapply(missing_atts, function(a) span(a, class="badge bg-danger me-1 fs-6"))
            ),
            h6("Counterexamples in Data", class="fw-bold text-muted small uppercase mb-2"),
            p("We scanned your formal context and found these object counterexamples:"),
            cex_table
          )
        )
      }
    })
  })

  # ===========================================================================
  # 7. RULE VISUALIZATION (fcaR implications → arules)
  # ===========================================================================
  arules_data <- reactive({
    vals$trigger
    req(vals$fc)
    if(is.null(vals$fc$implications)) return(NULL)
    n <- tryCatch(vals$fc$implications$cardinality(), error=function(e) -1)
    if(n<=0) return(NULL)
    tryCatch({ vals$fc$implications$to_arules() }, error=function(e) NULL)
  })
  arules_filtered <- reactive({
    req(arules_data())
    rules <- arules_data()
    if (!is.null(input$selectLHS)) rules <- subset(rules, subset = lhs %ain% input$selectLHS)
    if (!is.null(input$selectNotLHS)) rules <- subset(rules, subset = !(lhs %in% input$selectNotLHS))
    if (!is.null(input$selectRHS)) rules <- subset(rules, subset = rhs %ain% input$selectRHS)
    if (!is.null(input$selectNotRHS)) rules <- subset(rules, subset = !(rhs %in% input$selectNotRHS))
    if (!is.null(input$minLift) && input$minLift > 0) rules <- subset(rules, subset = lift >= input$minLift)
    if (!is.null(input$support) && input$support > 0) rules <- subset(rules, subset = support >= input$support)
    return(rules)
  })
  output$arulesPlotContainer <- renderUI({
    rules <- tryCatch(arules_filtered(), error=function(e) NULL)
    if(is.null(rules)) return(div(class="alert alert-info", "Compute implications first."))
    if(length(rules) == 0) return(div(class="alert alert-warning", "No rules satisfy filters."))
    
    if (input$arulesPlotType == "graph") {
      visNetworkOutput("arulesVisNet", height = "600px")
    } else if (input$arulesPlotType == "scatterplot") {
      plotly::plotlyOutput("arulesScatterplot", height = "600px")
    } else {
      plotly::plotlyOutput("arulesPlotly", height = "600px")
    }
  })
  output$arulesVisNet <- renderVisNetwork({
    req(arules_filtered())
    req(input$arulesPlotType == "graph")
    req(input$netColorMetric, input$netSizeMetric)
    rules <- arules_filtered()
    if(length(rules)>100) rules <- head(rules, n=100, by="lift")
    tryCatch({
      vn <- plot(rules, method="graph", engine="htmlwidget")
      nodes <- vn$x$nodes
      is_rule <- nodes$group == 2
      if (any(is_rule)) {
        rule_indices <- as.numeric(gsub("rule ", "", nodes$label[is_rule]))
        q <- quality(rules)[rule_indices, ]
        
        # 1. Custom Size Metric
        size_metric <- if (input$netSizeMetric == "support") q$support else q$lift
        size_metric[is.na(size_metric) | is.nan(size_metric) | !is.finite(size_metric)] <- 0
        if (diff(range(size_metric)) == 0) {
          norm_size <- rep(25, length(size_metric))
        } else {
          norm_size <- 10 + 30 * (size_metric - min(size_metric)) / diff(range(size_metric))
        }
        nodes$value[is_rule] <- norm_size
        
        # 2. Custom Color Metric
        color_metric <- if (input$netColorMetric == "support") q$support else q$lift
        color_metric[is.na(color_metric) | is.nan(color_metric) | !is.finite(color_metric)] <- 0
        pal <- colorRampPalette(c("#FED976", "#FD8D3C", "#E31A1C"))(100)
        if (diff(range(color_metric)) == 0) {
          nodes$color[is_rule] <- pal[50]
        } else {
          color_idx <- round(1 + 99 * (color_metric - min(color_metric)) / diff(range(color_metric)))
          nodes$color[is_rule] <- pal[color_idx]
        }
      }
      vn$x$nodes <- nodes
      
      vn %>% visNetwork::visOptions(
        nodesIdSelection = list(enabled = TRUE, main = "Select attribute or implication number"),
        highlightNearest = list(enabled = TRUE, degree = 1, hover = FALSE, labelOnly = FALSE)
      )
    }, error = function(e) NULL)
  })
  output$netGraphLegendUI <- renderUI({
    req(arules_filtered(), input$netColorMetric, input$netSizeMetric)
    rules <- arules_filtered()
    if(length(rules)>100) rules <- head(rules, n=100, by="lift")
    q <- quality(rules)
    
    color_label <- if (input$netColorMetric == "support") "Support" else "Lift"
    size_label <- if (input$netSizeMetric == "support") "Support" else "Lift"
    
    color_val <- if (input$netColorMetric == "support") q$support else q$lift
    size_val <- if (input$netSizeMetric == "support") q$support else q$lift
    color_val[is.na(color_val) | is.nan(color_val) | !is.finite(color_val)] <- 0
    size_val[is.na(size_val) | is.nan(size_val) | !is.finite(size_val)] <- 0
    
    min_c <- min(color_val)
    max_c <- max(color_val)
    min_s <- min(size_val)
    max_s <- max(size_val)
    
    color_range_text <- if(min_c == max_c) paste(round(min_c, 3)) else paste0(round(min_c, 3), " - ", round(max_c, 3))
    size_range_text <- if(min_s == max_s) paste(round(min_s, 3)) else paste0(round(min_s, 3), " - ", round(max_s, 3))
    
    tagList(
      hr(),
      h6("Legend", class="text-muted small mb-2"),
      div(class="d-flex align-items-center mb-2",
          span(style="width:20px; height:20px; background-color:#CBD2FC; border:1px solid #c0c0c0; display:inline-block; margin-right:8px; border-radius:3px;"),
          span("Attributes / Items", class="small text-muted")
      ),
      div(class="d-flex align-items-center mb-2",
          span(style="width:20px; height:20px; background-color:#FD8D3C; border-radius:50%; display:inline-block; margin-right:8px;"),
          span(paste("Rules (Color:", color_label, ")"), class="small text-muted")
      ),
      div(class="ps-4 small text-muted",
          div(style="height:8px; background:linear-gradient(90deg, #FED976 0%, #FD8D3C 50%, #E31A1C 100%); border-radius:3px; margin-bottom:4px;"),
          div(class="d-flex justify-content-between",
              span(round(min_c, 3)),
              span(round(max_c, 3))
          )
      ),
      div(class="d-flex align-items-center mt-2",
          span(class="small me-2 text-muted", paste("Rule Size:", size_label)),
          span(class="badge bg-secondary", size_range_text)
      )
    )
  })
  output$arulesScatterplot <- plotly::renderPlotly({
    req(arules_filtered())
    req(input$arulesPlotType == "scatterplot")
    req(input$scatterXMetric, input$scatterYMetric, input$scatterColorMetric, input$scatterSizeMetric)
    rules <- arules_filtered()
    
    # Extract quality metrics
    q <- quality(rules)
    lhs_size <- size(lhs(rules))
    rhs_size <- size(rhs(rules))
    
    # Helper to map inputs to metrics
    get_metric_values <- function(metric_name) {
      val <- if (metric_name == "support") {
        q$support
      } else if (metric_name == "lift") {
        q$lift
      } else if (metric_name == "lhs_size") {
        lhs_size
      } else if (metric_name == "rhs_size") {
        rhs_size
      } else {
        q$support
      }
      val[is.na(val) | is.nan(val) | !is.finite(val)] <- 0
      return(val)
    }
    
    x_val <- get_metric_values(input$scatterXMetric)
    y_val <- get_metric_values(input$scatterYMetric)
    color_val <- get_metric_values(input$scatterColorMetric)
    size_val <- get_metric_values(input$scatterSizeMetric)
    
    # Scale marker sizes nicely between 8 and 28
    scaled_size <- 8 + 20 * (size_val - min(size_val)) / max(c(diff(range(size_val)), 0.0001))
    
    # Generate clean labels for tooltip
    df_rules <- as(rules, "data.frame")
    rule_labels <- gsub("\\{", "", df_rules$rules)
    rule_labels <- gsub("\\}", "", rule_labels)
    rule_labels <- gsub("=>", " \u2192 ", rule_labels)
    
    # Build data frame for Plotly
    plot_df <- data.frame(
      Rule = rule_labels,
      X_Val = x_val,
      Y_Val = y_val,
      Color_Val = color_val,
      Scaled_Size = scaled_size,
      Support = q$support,
      Lift = q$lift,
      LHS_Size = lhs_size,
      RHS_Size = rhs_size,
      stringsAsFactors = FALSE
    )
    
    # Proper labels for metrics in colorbar/axes
    metric_label <- function(m) {
      if (m == "support") return("Support")
      if (m == "lift") return("Lift")
      if (m == "lhs_size") return("LHS Size")
      if (m == "rhs_size") return("RHS Size")
      return(m)
    }
    
    # Generate scatterplot using plotly
    p <- plotly::plot_ly(
      data = plot_df,
      x = ~X_Val,
      y = ~Y_Val,
      type = "scatter",
      mode = "markers",
      marker = list(
        size = ~Scaled_Size,
        color = ~Color_Val,
        colorscale = "Viridis",
        colorbar = list(title = metric_label(input$scatterColorMetric)),
        opacity = 0.8,
        line = list(color = "#ffffff", width = 1)
      ),
      text = ~paste0(
        "<b>Rule:</b> ", Rule, "<br>",
        "<b>Support:</b> ", round(Support, 4), "<br>",
        "<b>Lift:</b> ", round(Lift, 2), "<br>",
        "<b>LHS Size:</b> ", LHS_Size, "<br>",
        "<b>RHS Size:</b> ", RHS_Size
      ),
      hoverinfo = "text"
    ) %>%
      plotly::layout(
        title = paste("Rules Explorer (", metric_label(input$scatterXMetric), "vs", metric_label(input$scatterYMetric), ")"),
        xaxis = list(title = metric_label(input$scatterXMetric), gridcolor = "#f3f3f3"),
        yaxis = list(title = metric_label(input$scatterYMetric), gridcolor = "#f3f3f3"),
        hoverlabel = list(bgcolor = "#2c3e50", font = list(color = "#ffffff")),
        margin = list(t = 50, b = 50, l = 50, r = 50)
      )
    
    return(p)
  })
  output$arulesPlotly <- plotly::renderPlotly({
    req(arules_filtered())
    req(input$arulesPlotType == "grouped")
    rules <- arules_filtered()
    
    # 1. Group rules by their closure
    arules_labels <- rules@lhs@itemInfo$labels
    fcar_labels <- vals$fc$attributes
    lhs_data <- as(rules@lhs@data, "ngCMatrix")
    
    rule_closures <- list()
    for (i in 1:length(rules)) {
      active_indices <- which(lhs_data[, i])
      active_labels <- arules_labels[active_indices]
      valid_labels <- intersect(active_labels, fcar_labels)
      
      S <- Set$new(fcar_labels)
      if (length(valid_labels) > 0) {
        S$assign(attributes = valid_labels, values = rep(1, length(valid_labels)))
      }
      S_clo <- vals$fc$implications$closure(S)
      vec_clo <- as.numeric(S_clo$closure$get_vector())
      rule_closures[[i]] <- fcar_labels[vec_clo > 0]
    }
    
    closure_keys <- sapply(rule_closures, function(x) paste(sort(x), collapse = ", "))
    unique_closures <- unique(closure_keys)
    
    # Build data points by iterating over unique closures
    plot_data_list <- list()
    for (idx in 1:length(unique_closures)) {
      key <- unique_closures[idx]
      matching_rules_idx <- which(closure_keys == key)
      
      # For this group, collect all (attribute, support, lift, rule_str)
      group_rules <- rules[matching_rules_idx]
      q <- quality(group_rules)
      
      # Clean rule representations
      df_rules <- as(group_rules, "data.frame")
      rule_formulas <- gsub("\\{", "", df_rules$rules)
      rule_formulas <- gsub("\\}", "", rule_formulas)
      rule_formulas <- gsub("=>", " \u2192 ", rule_formulas)
      
      # Group LHS description for Y-axis (to show which premises lead to this closure)
      lhs_strings <- sapply(1:length(group_rules), function(r_idx) {
        lhs_atts <- arules_labels[which(as(group_rules@lhs@data, "ngCMatrix")[, r_idx])]
        paste0("{", paste(lhs_atts, collapse = ", "), "}")
      })
      unique_lhs_desc <- paste(unique(lhs_strings), collapse = " / ")
      
      # Y-axis label: Group name + its rule count (to prevent cutoffs)
      y_label <- paste0("Group ", idx, " (", length(matching_rules_idx), " rules)")
      
      # For each attribute in the closure, check if it is implied by any rule in this group
      for (att in rule_closures[[matching_rules_idx[1]]]) {
        rules_implying <- c()
        supps <- c()
        lifts <- c()
        
        for (r_idx in 1:length(group_rules)) {
          rhs_atts <- arules_labels[which(as(group_rules@rhs@data, "ngCMatrix")[, r_idx])]
          if (att %in% rhs_atts) {
            rules_implying <- c(rules_implying, rule_formulas[r_idx])
            supps <- c(supps, q$support[r_idx])
            lifts <- c(lifts, q$lift[r_idx])
          }
        }
        
        # If at least one rule implies this attribute, create a bubble point
        if (length(rules_implying) > 0) {
          plot_data_list[[length(plot_data_list) + 1]] <- data.frame(
            Attribute = att,
            Group = y_label,
            Closure = paste0("{", key, "}"),
            LHS = unique_lhs_desc,
            Support = mean(supps),
            Lift = mean(lifts),
            Rules = paste(rules_implying, collapse = "<br>"),
            stringsAsFactors = FALSE
          )
        }
      }
    }
    
    if (length(plot_data_list) == 0) {
      return(plotly::plot_ly() %>% plotly::layout(title = "No implications to display"))
    }
    
    plot_df <- do.call(rbind, plot_data_list)
    
    # Scale marker sizes nicely between 10 and 35 based on support
    scaled_size <- 10 + 25 * (plot_df$Support - min(plot_df$Support)) / max(c(diff(range(plot_df$Support)), 0.0001))
    plot_df$ScaledSize <- scaled_size
    
    # Plot using Plotly (Custom Grouped Matrix Bubble Chart)
    p <- plotly::plot_ly(
      data = plot_df,
      x = ~Attribute,
      y = ~Group,
      type = "scatter",
      mode = "markers",
      marker = list(
        size = ~ScaledSize,
        color = ~Lift,
        colorscale = "Viridis",
        colorbar = list(title = "Mean Lift"),
        opacity = 0.85,
        line = list(color = "#ffffff", width = 1.5)
      ),
      text = ~paste0(
        "<b>Rule Group:</b> ", Group, "<br>",
        "<b>Closure:</b> ", Closure, "<br>",
        "<b>LHS Premises:</b> ", LHS, "<br>",
        "<b>Implied Attribute:</b> ", Attribute, "<br>",
        "<b>Mean Support:</b> ", round(Support, 4), "<br>",
        "<b>Mean Lift:</b> ", round(Lift, 2), "<br><br>",
        "<b>Unified Rules:</b><br>", Rules
      ),
      hoverinfo = "text"
    ) %>%
      plotly::layout(
        title = "Semantic Grouped Matrix (Unified by Closures)",
        xaxis = list(title = "Implied Attributes (RHS)", gridcolor = "#f3f3f3"),
        yaxis = list(title = "Unified Rule Groups (by Closure)", gridcolor = "#f3f3f3", automargin = TRUE),
        hoverlabel = list(bgcolor = "#2c3e50", font = list(color = "#ffffff")),
        margin = list(t = 65, b = 65, l = 150, r = 50)
      )
    
    return(p)
  })
  output$arulesTable <- renderDT({
    req(arules_filtered())
    df <- as(arules_filtered(), "data.frame")
    df$rules <- gsub("\\{", "", df$rules)
    df$rules <- gsub("\\}", "", df$rules)
    df$rules <- gsub("=>", " → ", df$rules)
    
    # 1. Filter out columns that are completely empty (all NA, NaN, NULL, or "")
    empty_cols <- sapply(df, function(col) {
      all(is.na(col) | is.nan(col) | is.null(col) | col == "")
    })
    df <- df[, !empty_cols, drop = FALSE]
    
    # 2. Capitalize remaining column names for neatness
    names(df) <- toupper(names(df))
    
    # 3. Identify numeric columns programmatically
    num_cols <- names(df)[sapply(df, is.numeric)]
    # Exclude count or rule index from rounding decimals
    num_cols <- num_cols[!tolower(num_cols) %in% c("count", "rule")]
    
    datatable(df, rownames = FALSE, options = list(scrollX = TRUE)) %>% 
      formatRound(columns = num_cols, digits = 3)
  })

  convert_rules_to_fcaR_safe <- function(rules_obj, formal_context) {
    # Safety check: do not attempt conversion if there are no rules
    if (length(rules_obj) == 0) {
      stop("No rules to convert (0 rules mined).")
    }
    
    # Build transactions the same way as when mining, so labels always match
    fc_mat <- t(as.matrix(formal_context$I))
    # The item labels arules assigns are the column names of the transposed matrix,
    # which are the attribute names of the formal context
    fcar_labels <- formal_context$attributes
    arules_labels <- rules_obj@lhs@itemInfo$labels
    
    # Compute the permutation: map each fcaR attribute to its position in arules items
    p <- match(fcar_labels, arules_labels)
    if (any(is.na(p))) {
      stop(paste0(
        "Mismatch between arules items and fcaR attributes.\n",
        "arules labels: ", paste(arules_labels, collapse = ", "), "\n",
        "fcaR attributes: ", paste(fcar_labels, collapse = ", ")
      ))
    }
    
    lhs_mat <- as(rules_obj@lhs@data, "ngCMatrix")
    rhs_mat <- as(rules_obj@rhs@data, "ngCMatrix")
    
    lhs_aligned <- lhs_mat[p, , drop = FALSE]
    rhs_aligned <- rhs_mat[p, , drop = FALSE]
    lhs_aligned <- as(lhs_aligned, "dgCMatrix")
    rhs_aligned <- as(rhs_aligned, "dgCMatrix")
    
    new_imps <- ImplicationSet$new(attributes = formal_context$attributes, I = formal_context$I)
    new_imps$add(lhs_aligned, rhs_aligned)
    return(new_imps)
  }

  # ===========================================================================
  # 8. INTERACTIVE ATTRIBUTE EXPLORATION (CONEXP INSPIRATION)
  # ===========================================================================
  
  # Helper to parse string vectors safely
  clean_vector <- function(s) {
    if (is.null(s) || is.na(s) || s == "" || s == "{}") return(character(0))
    parts <- strsplit(s, ", ")[[1]]
    parts <- parts[parts != "" & parts != "{}"]
    return(parts)
  }
  
  # --- UI Conditionals ---
  output$isExplorationActive <- reactive({
    isTRUE(vals$exploration_active)
  })
  outputOptions(output, "isExplorationActive", suspendWhenHidden = FALSE)
  
  output$isExplorationComplete <- reactive({
    isTRUE(vals$exploration_complete)
  })
  outputOptions(output, "isExplorationComplete", suspendWhenHidden = FALSE)
  
  output$isRefutingActive <- reactive({
    isTRUE(vals$is_refuting)
  })
  outputOptions(output, "isRefutingActive", suspendWhenHidden = FALSE)

  output$refutingRuleKeyText <- renderUI({
    req(vals$current_question)
    code(vals$current_question$key)
  })

  output$cexAttributesSelectorUI <- renderUI({
    req(vals$fc, vals$current_question)
    q <- vals$current_question
    selected_atts <- clean_vector(q$lhs)
    checkboxGroupInput("cex_attributes", "Select the attributes that this object possesses:",
                       choices = vals$fc$attributes,
                       selected = selected_atts,
                       inline = TRUE)
  })
  
  # --- Badge and Counts ---
  output$explorationStatusBadge <- renderUI({
    if (isTRUE(vals$exploration_complete)) {
      span("Complete", class = "badge bg-success fs-6 px-3 py-2")
    } else if (!isTRUE(vals$exploration_active)) {
      span("Inactive", class = "badge bg-secondary fs-6 px-3 py-2")
    } else {
      span("Active", class = "badge bg-primary fs-6 px-3 py-2 blink")
    }
  })
  
  output$confirmedRulesCount <- renderUI({
    n <- length(vals$confirmed_rules)
    div(class="d-flex justify-content-between align-items-center mt-2",
        span("Confirmed Rules:", class="text-muted small"),
        span(n, class="badge bg-success"))
  })
  
  output$refutedRulesCount <- renderUI({
    n <- length(vals$refuted_rules)
    div(class="d-flex justify-content-between align-items-center mt-2",
        span("Refutations:", class="text-muted small"),
        span(n, class="badge bg-danger"))
  })
  
  # --- Question and Confirmed Rules List ---
  output$questionText <- renderUI({
    req(vals$current_question)
    q <- vals$current_question
    
    # Beautifully display LHS and RHS attributes
    lhs_clean <- clean_vector(q$lhs)
    rhs_clean <- clean_vector(q$rhs)
    
    lhs_html <- if (length(lhs_clean) == 0) {
      as.character(span("Ø", class="text-muted fw-bold"))
    } else {
      paste(sapply(lhs_clean, function(a) {
        as.character(span(a, class="badge bg-dark me-1 fs-6"))
      }), collapse = " ")
    }
    
    rhs_html <- paste(sapply(rhs_clean, function(a) {
      as.character(span(a, class="badge bg-primary me-1 fs-6"))
    }), collapse = " ")
    
    div(class="d-flex align-items-center justify-content-center flex-wrap py-3",
        div(class="text-center px-3", HTML(lhs_html)),
        div(class="px-2 fs-3 exploration-arrow", icon("arrow-right")),
        div(class="text-center px-3", HTML(rhs_html))
    )
  })
  
  output$listConfirmedRules <- renderUI({
    if (length(vals$confirmed_rules) == 0) {
      return(p(class="text-muted italic", "No rules confirmed yet."))
    }
    tags$ul(class="list-group list-group-flush",
            lapply(vals$confirmed_rules, function(r) {
              tags$li(class="list-group-item bg-transparent text-success fw-bold py-1",
                      icon("check-circle", class="me-2"), r)
            }))
  })

  # --- Start and Reset Observers ---
  observeEvent(input$btnStartExploration, {
    req(vals$fc)
    shinyjs::disable("btnStartExploration")
    showNotification("Initializing Attribute Exploration in the background...", 
                     type = "message", id = "bg_expl_notif", duration = NULL)
    
    vals$exploration_active <- TRUE
    vals$exploration_complete <- FALSE
    vals$is_refuting <- FALSE
    vals$confirmed_rules <- character(0)
    vals$refuted_rules <- character(0)
    
    fc_clon <- vals$fc$clone()
    future({
      library(fcaR)
      source("global.R")
      safe_find_implications(fc_clon)
      fc_clon
    }, seed = TRUE) %...>% (function(res_fc) {
      vals$fc <- res_fc
      implications_df(NULL)
      vals$trigger <- vals$trigger + 1
      
      removeNotification("bg_expl_notif")
      shinyjs::enable("btnStartExploration")
      shinyalert("Exploration Started", "Systematic knowledge acquisition is now active. Answer the questions on the right panel.", type = "info")
    }) %...!% (function(e) {
      vals$exploration_active <- FALSE
      removeNotification("bg_expl_notif")
      shinyjs::enable("btnStartExploration")
      shinyalert("Error", paste("Failed to initialize exploration:", e$message), type = "error")
    })
    NULL
  })
  
  observeEvent(input$btnResetExploration, {
    vals$exploration_active <- FALSE
    vals$exploration_complete <- FALSE
    vals$is_refuting <- FALSE
    vals$confirmed_rules <- character(0)
    vals$refuted_rules <- character(0)
    vals$current_question <- NULL
    
    shinyalert("Exploration Reset", "The interactive exploration state has been cleared.", type = "warning")
  })

  # --- Interactive exploration loop logic ---
  observe({
    req(vals$exploration_active, vals$fc)
    
    # If implications are not computed, find them
    if (is.null(vals$fc$implications)) {
      withProgress(message = "Computing canonical basis...", value = 0.5, {
        safe_find_implications(vals$fc)
      })
    }
    
    # Get current implications
    imp_set <- vals$fc$implications
    if (is.null(imp_set) || imp_set$cardinality() == 0) {
      vals$exploration_complete <- TRUE
      vals$current_question <- NULL
      implications_df(NULL)
      vals$trigger <- vals$trigger + 1
      return()
    }
    
    imps_df <- get_implications_dataframe(imp_set)
    
    # Generate keys for all current implications
    keys <- sapply(1:nrow(imps_df), function(i) {
      paste0(imps_df$`if`[i], " => ", imps_df$then[i])
    })
    
    # Find the first one not in confirmed_rules
    unconfirmed_idx <- which(!(keys %in% vals$confirmed_rules))
    
    if (length(unconfirmed_idx) == 0) {
      vals$exploration_complete <- TRUE
      vals$exploration_active <- FALSE  # Deactivate exploration to prevent infinite reactive loops
      vals$current_question <- NULL
      
      # Clear any stale filter selections that may crash arules after context change
      updateSelectInput(session, "selectLHS", selected = character(0))
      updateSelectInput(session, "selectNotLHS", selected = character(0))
      updateSelectInput(session, "selectRHS", selected = character(0))
      updateSelectInput(session, "selectNotRHS", selected = character(0))
      updateSliderInput(session, "support", value = 0)
      updateNumericInput(session, "minLift", value = 1.0)
      vals$filtered_imps <- NULL
      
      implications_df(get_implications_dataframe(vals$fc$implications))
      vals$trigger <- vals$trigger + 1
    } else {
      idx <- unconfirmed_idx[1]
      vals$exploration_complete <- FALSE
      vals$current_question <- list(
        idx = idx,
        lhs = imps_df$`if`[idx],
        rhs = imps_df$then[idx],
        key = keys[idx],
        lhs_vector = clean_vector(imps_df$`if`[idx]),
        rhs_vector = clean_vector(imps_df$then[idx])
      )
    }
  })

  # --- Confirm Rule ---
  observeEvent(input$btnConfirmRule, {
    req(vals$exploration_active, vals$current_question)
    q <- vals$current_question
    vals$confirmed_rules <- c(vals$confirmed_rules, q$key)
    showNotification("Implication confirmed as domain rule!", type = "message")
  })

  # --- Refute Rule with Counterexample ---
  observeEvent(input$btnRefuteRule, {
    req(vals$exploration_active, vals$current_question, vals$fc)
    q <- vals$current_question
    updateTextInput(session, "cex_name", value = paste0("Cex_", length(vals$refuted_rules) + 1))
    vals$is_refuting <- TRUE
  })

  # --- Cancel Refute Action ---
  observeEvent(input$btnCancelRefute, {
    vals$is_refuting <- FALSE
  })

  # --- Save Counterexample ---
  observeEvent(input$btnSaveCex, {
    req(vals$exploration_active, vals$current_question, vals$fc, input$cex_name)
    q <- vals$current_question
    
    cex_name <- trimws(input$cex_name)
    if (cex_name == "") {
      showNotification("Please provide a name for the counterexample.", type = "error")
      return()
    }
    
    # Check if object name already exists
    if (cex_name %in% vals$fc$objects) {
      showNotification("An object with this name already exists. Please choose a unique name.", type = "error")
      return()
    }
    
    selected_atts <- input$cex_attributes
    lhs_atts <- clean_vector(q$lhs)
    rhs_atts <- clean_vector(q$rhs)
    
    # Validation 1: Must possess all LHS attributes
    if (length(lhs_atts) > 0 && !all(lhs_atts %in% selected_atts)) {
      showNotification("Invalid Counterexample: It must possess ALL attributes in the LHS (premise).", type = "error")
      return()
    }
    
    # Validation 2: Must lack at least one RHS attribute
    if (length(rhs_atts) > 0 && all(rhs_atts %in% selected_atts)) {
      showNotification("Invalid Counterexample: It must LACK at least one attribute in the RHS (conclusion).", type = "error")
      return()
    }
    
    # All good! Update the context
    mat <- t(as.matrix(vals$fc$I))
    new_row <- matrix(0, nrow = 1, ncol = ncol(mat))
    colnames(new_row) <- colnames(mat)
    rownames(new_row) <- cex_name
    
    if (length(selected_atts) > 0) {
      new_row[1, selected_atts] <- 1
    }
    
    new_mat <- rbind(mat, new_row)
    
    # Re-instantiate formal context!
    vals$fc <- FormalContext$new(new_mat)
    
    # Clear computed implications and recalculate
    withProgress(message = "Re-evaluating implications with counterexample...", value = 0.5, {
      safe_find_implications(vals$fc)
      vals$fc$find_concepts()
    })
    
    # Save statistics
    vals$refuted_rules <- c(vals$refuted_rules, q$key)
    vals$is_refuting <- FALSE
    vals$trigger <- vals$trigger + 1
    implications_df(NULL)
    vals$stats_orig <- NULL
    
    shinyalert("Counterexample Added", paste0("Object '", cex_name, "' has been added to your formal context."), type = "success")
  })

  # ===========================================================================
  # 9. DUAL INTERACTIVE OBJECT EXPLORATION (GANTER ON TRANSPOSED CONTEXT)
  # ===========================================================================
  
  # --- UI Conditionals ---
  output$isObjExplorationActive <- reactive({
    isTRUE(vals$obj_exploration_active)
  })
  outputOptions(output, "isObjExplorationActive", suspendWhenHidden = FALSE)
  
  output$isObjExplorationComplete <- reactive({
    isTRUE(vals$obj_exploration_complete)
  })
  outputOptions(output, "isObjExplorationComplete", suspendWhenHidden = FALSE)
  
  # --- Badge and Counts ---
  output$objExplorationStatusBadge <- renderUI({
    if (!isTRUE(vals$obj_exploration_active)) {
      span("Inactive", class = "badge bg-secondary fs-6 px-3 py-2")
    } else if (isTRUE(vals$obj_exploration_complete)) {
      span("Complete", class = "badge bg-success fs-6 px-3 py-2")
    } else {
      span("Active", class = "badge bg-warning text-dark fs-6 px-3 py-2 blink")
    }
  })
  
  output$objConfirmedRulesCount <- renderUI({
    n <- length(vals$obj_confirmed_rules)
    div(class="d-flex justify-content-between align-items-center mt-2",
        span("Confirmed Object Rules:", class="text-muted small"),
        span(n, class="badge bg-success"))
  })
  
  output$objRefutedRulesCount <- renderUI({
    n <- length(vals$obj_refuted_rules)
    div(class="d-flex justify-content-between align-items-center mt-2",
        span("Refutations:", class="text-muted small"),
        span(n, class="badge bg-danger"))
  })
  
  # --- Question and Confirmed Rules List ---
  output$objQuestionText <- renderUI({
    req(vals$obj_current_question)
    q <- vals$obj_current_question
    
    lhs_clean <- clean_vector(q$lhs)
    rhs_clean <- clean_vector(q$rhs)
    
    lhs_html <- if (length(lhs_clean) == 0) {
      as.character(span("Ø", class="text-muted fw-bold"))
    } else {
      paste(sapply(lhs_clean, function(a) {
        as.character(span(a, class="badge bg-warning text-dark me-1 fs-6"))
      }), collapse = " ")
    }
    
    rhs_html <- paste(sapply(rhs_clean, function(a) {
      as.character(span(a, class="badge bg-primary me-1 fs-6"))
    }), collapse = " ")
    
    div(class="d-flex align-items-center justify-content-center flex-wrap py-3",
        div(class="text-center px-3", HTML(lhs_html)),
        div(class="px-2 fs-3 exploration-arrow", icon("arrow-right")),
        div(class="text-center px-3", HTML(rhs_html))
    )
  })
  
  output$listObjConfirmedRules <- renderUI({
    if (length(vals$obj_confirmed_rules) == 0) {
      return(p(class="text-muted italic", "No object rules confirmed yet."))
    }
    tags$ul(class="list-group list-group-flush",
            lapply(vals$obj_confirmed_rules, function(r) {
              tags$li(class="list-group-item bg-transparent text-success fw-bold py-1",
                      icon("check-circle", class="me-2"), r)
            }))
  })

  # --- Start and Reset Observers ---
  observeEvent(input$btnStartObjExploration, {
    req(vals$fc)
    vals$obj_exploration_active <- TRUE
    vals$obj_exploration_complete <- FALSE
    vals$obj_confirmed_rules <- character(0)
    vals$obj_refuted_rules <- character(0)
    
    withProgress(message = "Initializing Object Exploration (Transposing Context)...", value = 0.5, {
      # Transpose matrix: rows become attributes (so they are our new objects), cols become objects
      transposed_mat <- as.matrix(vals$fc$I)
      vals$fc_transposed <- FormalContext$new(transposed_mat)
      safe_find_implications(vals$fc_transposed)
    })
    
    shinyalert("Object Exploration Started", "Systematic dual knowledge acquisition is active. Answer the questions on the right panel.", type = "info")
  })
  
  observeEvent(input$btnResetObjExploration, {
    vals$obj_exploration_active <- FALSE
    vals$obj_exploration_complete <- FALSE
    vals$obj_confirmed_rules <- character(0)
    vals$obj_refuted_rules <- character(0)
    vals$obj_current_question <- NULL
    vals$fc_transposed <- NULL
    
    shinyalert("Exploration Reset", "The dual object exploration state has been cleared.", type = "warning")
  })

  # --- Interactive exploration loop logic ---
  observe({
    req(vals$obj_exploration_active, vals$fc_transposed)
    
    if (is.null(vals$fc_transposed$implications)) {
      withProgress(message = "Computing canonical object basis...", value = 0.5, {
        safe_find_implications(vals$fc_transposed)
      })
    }
    
    imp_set <- vals$fc_transposed$implications
    if (is.null(imp_set) || imp_set$cardinality() == 0) {
      vals$obj_exploration_complete <- TRUE
      vals$obj_current_question <- NULL
      return()
    }
    
    imps_df <- get_implications_dataframe(imp_set)
    
    keys <- sapply(1:nrow(imps_df), function(i) {
      paste0(imps_df$`if`[i], " => ", imps_df$then[i])
    })
    
    unconfirmed_idx <- which(!(keys %in% vals$obj_confirmed_rules))
    
    if (length(unconfirmed_idx) == 0) {
      vals$obj_exploration_complete <- TRUE
      vals$obj_current_question <- NULL
    } else {
      idx <- unconfirmed_idx[1]
      vals$obj_exploration_complete <- FALSE
      vals$obj_current_question <- list(
        idx = idx,
        lhs = imps_df$`if`[idx],
        rhs = imps_df$then[idx],
        key = keys[idx],
        lhs_vector = clean_vector(imps_df$`if`[idx]),
        rhs_vector = clean_vector(imps_df$then[idx])
      )
    }
  })

  # --- Confirm Rule ---
  observeEvent(input$btnConfirmObjRule, {
    req(vals$obj_exploration_active, vals$obj_current_question)
    q <- vals$obj_current_question
    vals$obj_confirmed_rules <- c(vals$obj_confirmed_rules, q$key)
    showNotification("Object implication confirmed as domain rule!", type = "message")
  })

  # --- Refute Rule with Counterexample ---
  observeEvent(input$btnRefuteObjRule, {
    req(vals$obj_exploration_active, vals$obj_current_question, vals$fc)
    q <- vals$obj_current_question
    
    selected_objs <- clean_vector(q$lhs)
    
    showModal(modalDialog(
      title = span(icon("times-circle", class="text-warning"), " Provide Counterexample Attribute"),
      size = "m",
      easyClose = FALSE,
      
      p(class="text-muted small", 
        "A valid counterexample attribute must be possessed by ALL objects in the Premise (LHS), but MUST NOT be possessed by at least one object in the Conclusion (RHS)."),
      
      div(class="p-3 bg-light border rounded mb-3",
        tags$strong("Object Implication being refuted:"), br(),
        code(q$key)
      ),
      
      textInput("cex_attr_name", "Name of the new Attribute", 
                value = paste0("NewAtt_", length(vals$obj_refuted_rules) + 1), 
                width = "100%"),
      
      checkboxGroupInput("cex_objects", "Select the objects that possess this new attribute:",
                         choices = vals$fc$objects,
                         selected = selected_objs,
                         inline = TRUE),
      
      footer = tagList(
        modalButton("Cancel"),
        actionButton("btnSaveObjCex", "Save Counterexample", class = "btn-warning")
      )
    ))
  })

  # --- Save Counterexample ---
  observeEvent(input$btnSaveObjCex, {
    req(vals$obj_exploration_active, vals$obj_current_question, vals$fc, input$cex_attr_name)
    q <- vals$obj_current_question
    
    cex_attr_name <- trimws(input$cex_attr_name)
    if (cex_attr_name == "") {
      showNotification("Please provide a name for the counterexample attribute.", type = "error")
      return()
    }
    
    if (cex_attr_name %in% vals$fc$attributes) {
      showNotification("An attribute with this name already exists. Please choose a unique name.", type = "error")
      return()
    }
    
    selected_objs <- input$cex_objects
    lhs_objs <- clean_vector(q$lhs)
    rhs_objs <- clean_vector(q$rhs)
    
    # Validation 1: Must be possessed by all LHS objects
    if (length(lhs_objs) > 0 && !all(lhs_objs %in% selected_objs)) {
      showNotification("Invalid Counterexample: The new attribute must be possessed by ALL objects in the LHS (premise).", type = "error")
      return()
    }
    
    # Validation 2: Must LACK at least one RHS object
    if (length(rhs_objs) > 0 && all(rhs_objs %in% selected_objs)) {
      showNotification("Invalid Counterexample: The new attribute must LACK at least one object in the RHS (conclusion).", type = "error")
      return()
    }
    
    # All good! Update the main context: we are adding a NEW COLUMN (attribute) to vals$fc!
    mat <- t(as.matrix(vals$fc$I)) # rows = objects, columns = attributes
    new_col <- matrix(0, nrow = nrow(mat), ncol = 1)
    colnames(new_col) <- cex_attr_name
    rownames(new_col) <- rownames(mat)
    
    if (length(selected_objs) > 0) {
      new_col[selected_objs, 1] <- 1
    }
    
    new_mat <- cbind(mat, new_col)
    
    # Re-instantiate formal contexts!
    vals$fc <- FormalContext$new(new_mat)
    
    transposed_mat <- as.matrix(vals$fc$I)
    vals$fc_transposed <- FormalContext$new(transposed_mat)
    
    # Clear computed implications and recalculate
    withProgress(message = "Re-evaluating object implications with counterexample...", value = 0.5, {
      safe_find_implications(vals$fc_transposed)
      vals$fc$find_concepts()
    })
    
    # Save statistics
    vals$obj_refuted_rules <- c(vals$obj_refuted_rules, q$key)
    vals$trigger <- vals$trigger + 1
    implications_df(NULL)
    vals$stats_orig <- NULL
    
    # Close modal
    removeModal()
    shinyalert("Counterexample Added", paste0("Attribute '", cex_attr_name, "' has been added to your formal context."), type = "success")
  })
}

Try the fcaR package in your browser

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

fcaR documentation built on July 27, 2026, 5:06 p.m.