library(shiny)

# ---- shared helpers -------------------------------------------------------

.numeric_choices <- function(df) {
  if (is.null(df)) return(character(0))
  names(df)[vapply(df, function(x) is.numeric(x) || !all(is.na(suppressWarnings(as.numeric(x)))),
                    logical(1))]
}

.all_choices <- function(df) {
  if (is.null(df)) return(character(0))
  names(df)
}

# accumulator for the combined session report; each analysis run appends
# a named entry here (used by the "Download full report" feature)
report_env_key <- "biocharkitgui_report_store"

# ---- reusable "upload Excel" module ---------------------------------------

mod_upload_ui <- function(id, label = "1. Upload Excel file (.xlsx / .xls)") {
  ns <- NS(id)
  tagList(
    fileInput(ns("file"), label, accept = c(".xlsx", ".xls")),
    uiOutput(ns("sheet_ui")),
    tags$strong("Preview (first 5 rows):"),
    tableOutput(ns("preview"))
  )
}

mod_upload_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns

    sheets <- reactive({
      req(input$file)
      tryCatch(gui_list_sheets(input$file$datapath),
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    })

    output$sheet_ui <- renderUI({
      sh <- sheets()
      req(sh)
      if (length(sh) > 1) {
        selectInput(ns("sheet"), "Sheet", choices = sh)
      }
    })

    data <- reactive({
      req(input$file)
      sh <- if (!is.null(input$sheet)) input$sheet else 1
      tryCatch(gui_read_excel(input$file$datapath, sh),
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    })

    output$preview <- renderTable({
      req(data())
      gui_preview(data(), 5)
    })

    data
  })
}

# ---- UI ---------------------------------------------------------------

app_css <- "
.navbar { background-color: #3f5e3f !important; border-color: #2f4a2f; }
.navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { color: #ffffff !important; font-weight: 600; }
.navbar-default .navbar-nav>.active>a { background-color: #2f4a2f !important; }
.btn-run { background-color: #3f5e3f; color: white; }
h4 { color: #2f4a2f; }
.batch-note { color: #555; font-style: italic; font-size: 90%; }
"

ui <- navbarPage(
  title = "biocharkit GUI",
  theme = NULL,
  header = tags$head(tags$style(HTML(app_css))),

  tabPanel("Home",
    fluidPage(
      h3("biocharkit \u2014 no-code biochar & adsorption data analysis"),
      p("Upload an Excel workbook on any tab above, map your column names to the ",
        "required variables, and run the analysis \u2014 no R code required."),
      tags$ul(
        tags$li(strong("Sample ID Parser"), " \u2014 split IDs like SBC450-30 into feedstock / temperature / residence time"),
        tags$li(strong("Adsorption Capacity"), " \u2014 qe and % removal from C0, Ce, volume, mass"),
        tags$li(strong("Isotherms"), " \u2014 Langmuir, Freundlich, Temkin, Dubinin-Radushkevich, or Sips; single-sample or batch across many samples at once"),
        tags$li(strong("Kinetics"), " \u2014 pseudo-first/second-order, Elovich, or intraparticle diffusion; single-sample or batch"),
        tags$li(strong("Thermodynamics"), " \u2014 van't Hoff analysis (\u0394H\u00b0, \u0394S\u00b0, \u0394G\u00b0) across temperatures"),
        tags$li(strong("FTIR"), " \u2014 baseline correction, automatic peak picking, functional-group density, and manual peak assignment"),
        tags$li(strong("XRD"), " \u2014 crystallinity index from known peak areas, or by deconvolving a full pattern"),
        tags$li(strong("BET Surface Area"), " \u2014 multi-point BET from gas adsorption data"),
        tags$li(strong("TGA Analysis"), " \u2014 DTG curve with auto-detected decomposition peaks, moisture/VM/ash/fixed-carbon straight off a curve (single-sample or batch), and Kissinger kinetics from multi-heating-rate data"),
        tags$li(strong("Proximate / Ultimate Analysis"), " \u2014 moisture/VM/ash/fixed carbon and H/C, O/C, N/C atomic ratios"),
        tags$li(strong("Correlation Matrix"), " \u2014 Spearman / Pearson / Kendall correlations with p-values")
      ),
      p("Results tables and 600 dpi TIFF figures can be downloaded from each tab."),
      hr(),
      h4("Combined report"),
      p("Every analysis you run in this session is added to a running report. ",
        "Download everything so far as a single HTML file:"),
      downloadButton("report_download", "Download full session report (HTML)"),
      p(class = "batch-note", "The report updates automatically as you run more analyses \u2014 come back to this tab any time."),
      p(em("Powered by the biocharkit R package."))
    )
  ),

  tabPanel("Sample ID Parser",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("parse"),
        uiOutput("parse_col_ui"),
        actionButton("parse_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("Parsed sample IDs"),
        DT::dataTableOutput("parse_table"),
        downloadButton("parse_download", "Download results (CSV)")
      )
    )
  ),

  tabPanel("Adsorption Capacity",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("ads"),
        uiOutput("ads_col_ui"),
        actionButton("ads_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("Adsorption capacity (qe) and removal efficiency"),
        DT::dataTableOutput("ads_table"),
        downloadButton("ads_download", "Download results (CSV)")
      )
    )
  ),

  tabPanel("Isotherms",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("iso"),
        checkboxInput("iso_batch", "Batch mode (fit each group in a sample-ID column separately)", FALSE),
        uiOutput("iso_col_ui"),
        radioButtons("iso_model", "Model",
                     c("Langmuir" = "langmuir", "Freundlich" = "freundlich",
                       "Temkin" = "temkin", "Dubinin-Radushkevich" = "dr", "Sips" = "sips")),
        actionButton("iso_run", "Fit isotherm", class = "btn-run")
      ),
      mainPanel(
        h4("Fitted parameters"),
        DT::dataTableOutput("iso_params"),
        conditionalPanel("input.iso_batch == false", plotOutput("iso_plot")),
        downloadButton("iso_download_csv", "Download parameters (CSV)"),
        conditionalPanel("input.iso_batch == false",
          downloadButton("iso_download_tiff", "Download plot (600 dpi TIFF)"))
      )
    )
  ),

  tabPanel("Kinetics",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("kin"),
        checkboxInput("kin_batch", "Batch mode (fit each group in a sample-ID column separately)", FALSE),
        uiOutput("kin_col_ui"),
        radioButtons("kin_model", "Model",
                     c("Pseudo-first-order" = "pfo", "Pseudo-second-order" = "pso",
                       "Elovich" = "elovich", "Intraparticle diffusion" = "intraparticle")),
        actionButton("kin_run", "Fit kinetics", class = "btn-run")
      ),
      mainPanel(
        h4("Fitted parameters"),
        DT::dataTableOutput("kin_params"),
        conditionalPanel("input.kin_batch == false", plotOutput("kin_plot")),
        downloadButton("kin_download_csv", "Download parameters (CSV)"),
        conditionalPanel("input.kin_batch == false",
          downloadButton("kin_download_tiff", "Download plot (600 dpi TIFF)"))
      )
    )
  ),

  tabPanel("Thermodynamics",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("thermo", "1. Upload temperature + distribution coefficient (Kc) data"),
        uiOutput("thermo_col_ui"),
        actionButton("thermo_run", "Run van't Hoff analysis", class = "btn-run")
      ),
      mainPanel(
        h4("Thermodynamic parameters"),
        tableOutput("thermo_summary"),
        h4("\u0394G\u00b0 at each temperature"),
        DT::dataTableOutput("thermo_table"),
        downloadButton("thermo_download", "Download table (CSV)")
      )
    )
  ),

  tabPanel("FTIR \u2014 Functional Groups",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("ftirspec", "1. Upload full spectrum (wavenumber + intensity columns)"),
        uiOutput("ftirspec_col_ui"),
        checkboxInput("ftirspec_baseline", "Apply baseline correction first", TRUE),
        conditionalPanel("input.ftirspec_baseline == true",
          radioButtons("ftirspec_baseline_method", "Baseline method",
                       c("Linear (endpoints)" = "linear", "Rolling minimum" = "rolling_min"))),
        actionButton("ftirspec_run", "Run", class = "btn-run"),
        hr(),
        actionButton("ftirspec_autopeaks", "Auto-detect peaks")
      ),
      mainPanel(
        h4("Functional group density"),
        DT::dataTableOutput("ftirspec_table"),
        plotOutput("ftirspec_plot"),
        downloadButton("ftirspec_download_csv", "Download table (CSV)"),
        downloadButton("ftirspec_download_tiff", "Download spectrum plot (600 dpi TIFF)"),
        hr(),
        h4("Auto-detected peaks"),
        DT::dataTableOutput("ftirspec_autopeaks_table")
      )
    )
  ),

  tabPanel("FTIR \u2014 Peak Assignment",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("ftirpeak", "1. Upload a column of picked peak wavenumbers"),
        uiOutput("ftirpeak_col_ui"),
        actionButton("ftirpeak_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("Peak \u2192 functional group assignment"),
        DT::dataTableOutput("ftirpeak_table"),
        downloadButton("ftirpeak_download", "Download results (CSV)")
      )
    )
  ),

  tabPanel("XRD Crystallinity",
    sidebarLayout(
      sidebarPanel(
        radioButtons("xrd_mode", "I have\u2026",
                     c("Crystalline/amorphous peak areas already" = "areas",
                       "A full 2theta/intensity pattern to deconvolve" = "deconvolve")),
        conditionalPanel("input.xrd_mode == 'areas'",
          mod_upload_ui("xrd"),
          uiOutput("xrd_col_ui")
        ),
        conditionalPanel("input.xrd_mode == 'deconvolve'",
          mod_upload_ui("xrdpattern", "1. Upload full pattern (2theta + intensity columns)"),
          uiOutput("xrdpattern_col_ui"),
          textInput("xrd_peak_centers", "Approx. crystalline peak centers (2theta, comma-separated)",
                    placeholder = "e.g. 22, 29")
        ),
        actionButton("xrd_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("Crystallinity index"),
        DT::dataTableOutput("xrd_table"),
        downloadButton("xrd_download", "Download results (CSV)")
      )
    )
  ),

  tabPanel("BET Surface Area",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("bet", "1. Upload P/P0 and quantity-adsorbed columns"),
        uiOutput("bet_col_ui"),
        actionButton("bet_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("BET results"),
        tableOutput("bet_table"),
        downloadButton("bet_download", "Download results (CSV)")
      )
    )
  ),

  tabPanel("TGA Analysis",
    sidebarLayout(
      sidebarPanel(
        h4("TGA curve"),
        mod_upload_ui("tga", "Upload a full TGA curve (temperature + weight % columns)"),
        checkboxInput("tga_batch", "Batch mode (compute stages for each sample-ID group separately)", FALSE),
        uiOutput("tga_col_ui"),
        conditionalPanel("input.tga_batch == false",
          numericInput("tga_min_prom", "Min. DTG peak prominence (%/\u00b0C)", value = 0, min = 0, step = 0.01)),
        actionButton("tga_run", "Run TGA analysis", class = "btn-run"),
        hr(),
        h4("Kissinger kinetics (multi-heating-rate)"),
        p(class = "batch-note", "Needs the DTG peak temperature of the same decomposition ",
          "step at 3+ heating rates \u2014 not a single curve. Run the panel above once per ",
          "heating rate, note the peak temperature, then upload a small table here."),
        mod_upload_ui("tgakis", "Upload heating-rate + peak-temperature table"),
        uiOutput("tgakis_col_ui"),
        actionButton("tgakis_run", "Fit Kissinger kinetics", class = "btn-run")
      ),
      mainPanel(
        conditionalPanel("input.tga_batch == false",
          h4("Proximate analysis (from TGA curve)"),
          tableOutput("tga_stages_table"),
          plotOutput("tga_plot"),
          downloadButton("tga_download_csv", "Download proximate results (CSV)"),
          downloadButton("tga_download_tiff", "Download TG/DTG plot (600 dpi TIFF)"),
          hr(),
          h4("Auto-detected DTG peaks"),
          DT::dataTableOutput("tga_peaks_table")
        ),
        conditionalPanel("input.tga_batch == true",
          h4("Proximate analysis per sample (batch)"),
          DT::dataTableOutput("tga_batch_table"),
          downloadButton("tga_batch_download", "Download batch results (CSV)")
        ),
        hr(),
        h4("Kissinger kinetics results"),
        tableOutput("tgakis_table"),
        downloadButton("tgakis_download", "Download Kissinger results (CSV)")
      )
    )
  ),

  tabPanel("Proximate / Ultimate Analysis",
    sidebarLayout(
      sidebarPanel(
        h4("Proximate analysis"),
        mod_upload_ui("prox", "Upload moisture/VM/ash % columns"),
        uiOutput("prox_col_ui"),
        actionButton("prox_run", "Run proximate analysis", class = "btn-run"),
        hr(),
        h4("Ultimate analysis"),
        mod_upload_ui("ult", "Upload C/H/O(/N) % columns"),
        uiOutput("ult_col_ui"),
        actionButton("ult_run", "Run ultimate analysis", class = "btn-run")
      ),
      mainPanel(
        h4("Proximate analysis results"),
        DT::dataTableOutput("prox_table"),
        downloadButton("prox_download", "Download proximate results (CSV)"),
        hr(),
        h4("Ultimate analysis results (atomic ratios)"),
        DT::dataTableOutput("ult_table"),
        downloadButton("ult_download", "Download ultimate results (CSV)")
      )
    )
  ),

  tabPanel("Correlation Matrix",
    sidebarLayout(
      sidebarPanel(
        mod_upload_ui("corr"),
        uiOutput("corr_col_ui"),
        radioButtons("corr_method", "Method",
                     c("Spearman" = "spearman", "Pearson" = "pearson", "Kendall" = "kendall")),
        actionButton("corr_run", "Run", class = "btn-run")
      ),
      mainPanel(
        h4("Pairwise correlations"),
        DT::dataTableOutput("corr_table"),
        downloadButton("corr_download", "Download results (CSV)")
      )
    )
  )
)

# ---- server -------------------------------------------------------------

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

  # Accumulator for the combined session report
  store <- reactiveValues(
    sample_ids = NULL, adsorption = NULL, isotherm = NULL, kinetics = NULL,
    thermo = NULL, ftir_functional_groups = NULL, ftir_peak_assignment = NULL,
    xrd = NULL, bet = NULL, tga = NULL, tga_kissinger = NULL,
    proximate = NULL, ultimate = NULL, correlation = NULL
  )

  ## ---- Sample ID Parser ----
  parse_data <- mod_upload_server("parse")
  output$parse_col_ui <- renderUI({
    req(parse_data())
    selectInput("parse_id_col", "2. Sample ID column", choices = .all_choices(parse_data()))
  })
  parse_result <- eventReactive(input$parse_run, {
    req(parse_data(), input$parse_id_col)
    tryCatch(gui_parse_ids(parse_data(), input$parse_id_col),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(parse_result(), { store$sample_ids <- parse_result() })
  output$parse_table <- DT::renderDataTable({ req(parse_result()); parse_result() },
                                              options = list(pageLength = 10))
  output$parse_download <- downloadHandler(
    filename = function() "parsed_sample_ids.csv",
    content = function(file) { req(parse_result()); gui_write_csv(parse_result(), file) }
  )

  ## ---- Adsorption Capacity ----
  ads_data <- mod_upload_server("ads")
  output$ads_col_ui <- renderUI({
    req(ads_data())
    ch <- .numeric_choices(ads_data())
    all_ch <- .all_choices(ads_data())
    tagList(
      selectInput("ads_c0", "2. Initial concentration C0 (mg/L)", choices = ch),
      selectInput("ads_ce", "3. Equilibrium concentration Ce (mg/L)", choices = ch),
      selectInput("ads_v", "4. Solution volume V (L)", choices = ch),
      selectInput("ads_m", "5. Adsorbent mass m (g)", choices = ch),
      selectInput("ads_id", "6. (Optional) sample ID column", choices = c("(none)", all_ch))
    )
  })
  ads_result <- eventReactive(input$ads_run, {
    req(ads_data(), input$ads_c0, input$ads_ce, input$ads_v, input$ads_m)
    idc <- if (is.null(input$ads_id) || input$ads_id == "(none)") NULL else input$ads_id
    tryCatch(gui_adsorption(ads_data(), input$ads_c0, input$ads_ce, input$ads_v, input$ads_m, idc),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(ads_result(), { store$adsorption <- ads_result() })
  output$ads_table <- DT::renderDataTable({ req(ads_result()); ads_result() },
                                            options = list(pageLength = 10))
  output$ads_download <- downloadHandler(
    filename = function() "adsorption_results.csv",
    content = function(file) { req(ads_result()); gui_write_csv(ads_result(), file) }
  )

  ## ---- Isotherms ----
  iso_data <- mod_upload_server("iso")
  output$iso_col_ui <- renderUI({
    req(iso_data())
    ch <- .numeric_choices(iso_data())
    all_ch <- .all_choices(iso_data())
    if (isTRUE(input$iso_batch)) {
      tagList(
        selectInput("iso_group", "2. Sample ID / group column", choices = all_ch),
        selectInput("iso_ce", "3. Equilibrium concentration Ce (mg/L)", choices = ch),
        selectInput("iso_qe", "4. Adsorption capacity qe (mg/g)", choices = ch)
      )
    } else {
      tagList(
        selectInput("iso_ce", "2. Equilibrium concentration Ce (mg/L)", choices = ch),
        selectInput("iso_qe", "3. Adsorption capacity qe (mg/g)", choices = ch)
      )
    }
  })
  iso_result <- eventReactive(input$iso_run, {
    req(iso_data(), input$iso_ce, input$iso_qe)
    if (isTRUE(input$iso_batch)) {
      req(input$iso_group)
      tryCatch(list(batch = TRUE, model = input$iso_model,
                    params = gui_isotherm_batch(iso_data(), input$iso_group, input$iso_ce, input$iso_qe, input$iso_model)),
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL },
               warning = function(w) { showNotification(conditionMessage(w), type = "warning")
                 tryCatch(list(batch = TRUE, model = input$iso_model,
                               params = suppressWarnings(gui_isotherm_batch(iso_data(), input$iso_group, input$iso_ce, input$iso_qe, input$iso_model))),
                          error = function(e) NULL) }
      )
    } else {
      tryCatch({ r <- gui_isotherm(iso_data(), input$iso_ce, input$iso_qe, input$iso_model)
                 c(list(batch = FALSE), r) },
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    }
  })
  observeEvent(iso_result(), {
    r <- iso_result(); req(r)
    plot_path <- if (!r$batch) gui_snapshot_png(biocharkit::plot_isotherm(r$fit, r$Ce, r$qe, model = r$model)) else NULL
    store$isotherm <- list(model = r$model, batch = r$batch, params = r$params, plot_path = plot_path)
  })
  output$iso_params <- DT::renderDataTable({ req(iso_result()); iso_result()$params },
                                             options = list(pageLength = 10))
  output$iso_plot <- renderPlot({
    req(iso_result()); r <- iso_result(); req(!r$batch)
    biocharkit::plot_isotherm(r$fit, r$Ce, r$qe, model = r$model)
  })
  output$iso_download_csv <- downloadHandler(
    filename = function() "isotherm_parameters.csv",
    content = function(file) { req(iso_result()); gui_write_csv(iso_result()$params, file) }
  )
  output$iso_download_tiff <- downloadHandler(
    filename = function() "isotherm_plot.tif",
    content = function(file) {
      req(iso_result()); r <- iso_result(); req(!r$batch)
      biocharkit::save_tiff(biocharkit::plot_isotherm(r$fit, r$Ce, r$qe, model = r$model), filename = file)
    }
  )

  ## ---- Kinetics ----
  kin_data <- mod_upload_server("kin")
  output$kin_col_ui <- renderUI({
    req(kin_data())
    ch <- .numeric_choices(kin_data())
    all_ch <- .all_choices(kin_data())
    if (isTRUE(input$kin_batch)) {
      tagList(
        selectInput("kin_group", "2. Sample ID / group column", choices = all_ch),
        selectInput("kin_t", "3. Contact time t", choices = ch),
        selectInput("kin_qt", "4. Adsorption capacity at time t, qt (mg/g)", choices = ch)
      )
    } else {
      tagList(
        selectInput("kin_t", "2. Contact time t", choices = ch),
        selectInput("kin_qt", "3. Adsorption capacity at time t, qt (mg/g)", choices = ch)
      )
    }
  })
  kin_result <- eventReactive(input$kin_run, {
    req(kin_data(), input$kin_t, input$kin_qt)
    if (isTRUE(input$kin_batch)) {
      req(input$kin_group)
      tryCatch(list(batch = TRUE, model = input$kin_model,
                    params = gui_kinetics_batch(kin_data(), input$kin_group, input$kin_t, input$kin_qt, input$kin_model)),
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL },
               warning = function(w) { showNotification(conditionMessage(w), type = "warning")
                 tryCatch(list(batch = TRUE, model = input$kin_model,
                               params = suppressWarnings(gui_kinetics_batch(kin_data(), input$kin_group, input$kin_t, input$kin_qt, input$kin_model))),
                          error = function(e) NULL) }
      )
    } else {
      tryCatch({ r <- gui_kinetics(kin_data(), input$kin_t, input$kin_qt, input$kin_model)
                 c(list(batch = FALSE), r) },
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    }
  })
  observeEvent(kin_result(), {
    r <- kin_result(); req(r)
    plot_path <- if (!r$batch) gui_snapshot_png(biocharkit::plot_kinetics(r$fit, r$t, r$qt, model = r$model)) else NULL
    store$kinetics <- list(model = r$model, batch = r$batch, params = r$params, plot_path = plot_path)
  })
  output$kin_params <- DT::renderDataTable({ req(kin_result()); kin_result()$params },
                                             options = list(pageLength = 10))
  output$kin_plot <- renderPlot({
    req(kin_result()); r <- kin_result(); req(!r$batch)
    biocharkit::plot_kinetics(r$fit, r$t, r$qt, model = r$model)
  })
  output$kin_download_csv <- downloadHandler(
    filename = function() "kinetics_parameters.csv",
    content = function(file) { req(kin_result()); gui_write_csv(kin_result()$params, file) }
  )
  output$kin_download_tiff <- downloadHandler(
    filename = function() "kinetics_plot.tif",
    content = function(file) {
      req(kin_result()); r <- kin_result(); req(!r$batch)
      biocharkit::save_tiff(biocharkit::plot_kinetics(r$fit, r$t, r$qt, model = r$model), filename = file)
    }
  )

  ## ---- Thermodynamics ----
  thermo_data <- mod_upload_server("thermo")
  output$thermo_col_ui <- renderUI({
    req(thermo_data())
    ch <- .numeric_choices(thermo_data())
    tagList(
      selectInput("thermo_temp", "2. Temperature column (Kelvin)", choices = ch),
      selectInput("thermo_kc", "3. Distribution coefficient Kc column", choices = ch)
    )
  })
  thermo_result <- eventReactive(input$thermo_run, {
    req(thermo_data(), input$thermo_temp, input$thermo_kc)
    tryCatch(gui_vant_hoff(thermo_data(), input$thermo_temp, input$thermo_kc),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(thermo_result(), { store$thermo <- thermo_result() })
  output$thermo_summary <- renderTable({
    req(thermo_result()); r <- thermo_result()
    data.frame(deltaH_kJmol = r$deltaH_kJmol, deltaS_Jmolk = r$deltaS_Jmolk, R2 = r$R2)
  })
  output$thermo_table <- DT::renderDataTable({ req(thermo_result()); thermo_result()$table },
                                               options = list(pageLength = 10))
  output$thermo_download <- downloadHandler(
    filename = function() "vant_hoff_results.csv",
    content = function(file) { req(thermo_result()); gui_write_csv(thermo_result()$table, file) }
  )

  ## ---- FTIR: Functional Groups (full spectrum) ----
  ftirspec_data <- mod_upload_server("ftirspec")
  output$ftirspec_col_ui <- renderUI({
    req(ftirspec_data())
    ch <- .numeric_choices(ftirspec_data())
    tagList(
      selectInput("ftirspec_wn", "2. Wavenumber column (cm-1)", choices = ch),
      selectInput("ftirspec_int", "3. Intensity / absorbance column", choices = ch)
    )
  })
  ftirspec_spectrum <- reactive({
    req(ftirspec_data(), input$ftirspec_wn, input$ftirspec_int)
    if (isTRUE(input$ftirspec_baseline)) {
      gui_ftir_baseline(ftirspec_data(), input$ftirspec_wn, input$ftirspec_int, input$ftirspec_baseline_method)
    } else {
      data.frame(wavenumber_cm1 = ftirspec_data()[[input$ftirspec_wn]],
                 intensity = ftirspec_data()[[input$ftirspec_int]])
    }
  })
  ftirspec_result <- eventReactive(input$ftirspec_run, {
    tryCatch(list(table = biocharkit::functional_group_density(ftirspec_spectrum()),
                  spectrum = ftirspec_spectrum()),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  ftirspec_peaks <- eventReactive(input$ftirspec_autopeaks, {
    tryCatch(biocharkit::find_ftir_peaks(ftirspec_spectrum()),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(ftirspec_result(), {
    plot_path <- gui_snapshot_png(biocharkit::plot_ftir_spectrum(ftirspec_result()$spectrum))
    store$ftir_functional_groups <- list(table = ftirspec_result()$table, plot_path = plot_path,
                                          autopeaks = if (!is.null(ftirspec_peaks())) ftirspec_peaks() else NULL)
  })
  observeEvent(ftirspec_peaks(), {
    if (!is.null(store$ftir_functional_groups)) {
      store$ftir_functional_groups$autopeaks <- ftirspec_peaks()
    }
  })
  output$ftirspec_table <- DT::renderDataTable({ req(ftirspec_result()); ftirspec_result()$table },
                                                 options = list(pageLength = 10))
  output$ftirspec_plot <- renderPlot({
    req(ftirspec_result())
    biocharkit::plot_ftir_spectrum(ftirspec_result()$spectrum)
  })
  output$ftirspec_autopeaks_table <- DT::renderDataTable({ req(ftirspec_peaks()); ftirspec_peaks() },
                                                            options = list(pageLength = 10))
  output$ftirspec_download_csv <- downloadHandler(
    filename = function() "ftir_functional_group_density.csv",
    content = function(file) { req(ftirspec_result()); gui_write_csv(ftirspec_result()$table, file) }
  )
  output$ftirspec_download_tiff <- downloadHandler(
    filename = function() "ftir_spectrum.tif",
    content = function(file) {
      req(ftirspec_result())
      biocharkit::save_tiff(biocharkit::plot_ftir_spectrum(ftirspec_result()$spectrum), filename = file)
    }
  )

  ## ---- FTIR: Peak Assignment ----
  ftirpeak_data <- mod_upload_server("ftirpeak")
  output$ftirpeak_col_ui <- renderUI({
    req(ftirpeak_data())
    selectInput("ftirpeak_col", "2. Peak wavenumber column", choices = .numeric_choices(ftirpeak_data()))
  })
  ftirpeak_result <- eventReactive(input$ftirpeak_run, {
    req(ftirpeak_data(), input$ftirpeak_col)
    tryCatch(gui_ftir_peaks(ftirpeak_data(), input$ftirpeak_col),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(ftirpeak_result(), { store$ftir_peak_assignment <- ftirpeak_result() })
  output$ftirpeak_table <- DT::renderDataTable({ req(ftirpeak_result()); ftirpeak_result() },
                                                 options = list(pageLength = 10))
  output$ftirpeak_download <- downloadHandler(
    filename = function() "ftir_peak_assignment.csv",
    content = function(file) { req(ftirpeak_result()); gui_write_csv(ftirpeak_result(), file) }
  )

  ## ---- XRD Crystallinity ----
  xrd_data <- mod_upload_server("xrd")
  output$xrd_col_ui <- renderUI({
    req(xrd_data())
    ch <- .numeric_choices(xrd_data())
    all_ch <- .all_choices(xrd_data())
    tagList(
      selectInput("xrd_cryst", "2. Crystalline peak area column", choices = ch),
      selectInput("xrd_amorph", "3. Amorphous peak area column", choices = ch),
      selectInput("xrd_id", "4. (Optional) sample ID column", choices = c("(none)", all_ch))
    )
  })
  xrdpattern_data <- mod_upload_server("xrdpattern")
  output$xrdpattern_col_ui <- renderUI({
    req(xrdpattern_data())
    ch <- .numeric_choices(xrdpattern_data())
    tagList(
      selectInput("xrdpattern_tt", "2. 2theta column", choices = ch),
      selectInput("xrdpattern_int", "3. Intensity column", choices = ch)
    )
  })
  xrd_result <- eventReactive(input$xrd_run, {
    if (identical(input$xrd_mode, "areas")) {
      req(xrd_data(), input$xrd_cryst, input$xrd_amorph)
      idc <- if (is.null(input$xrd_id) || input$xrd_id == "(none)") NULL else input$xrd_id
      tryCatch(gui_xrd_ci(xrd_data(), input$xrd_cryst, input$xrd_amorph, idc),
               error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    } else {
      req(xrdpattern_data(), input$xrdpattern_tt, input$xrdpattern_int, input$xrd_peak_centers)
      centers <- suppressWarnings(as.numeric(trimws(strsplit(input$xrd_peak_centers, ",")[[1]])))
      tryCatch({
        res <- gui_xrd_deconvolve(xrdpattern_data(), input$xrdpattern_tt, input$xrdpattern_int, centers)
        cbind(res$peaks, crystallinity_index = res$crystallinity_index, R2 = res$R2)
      }, error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
    }
  })
  observeEvent(xrd_result(), { store$xrd <- xrd_result() })
  output$xrd_table <- DT::renderDataTable({ req(xrd_result()); xrd_result() },
                                            options = list(pageLength = 10))
  output$xrd_download <- downloadHandler(
    filename = function() "xrd_crystallinity.csv",
    content = function(file) { req(xrd_result()); gui_write_csv(xrd_result(), file) }
  )

  ## ---- BET Surface Area ----
  bet_data <- mod_upload_server("bet")
  output$bet_col_ui <- renderUI({
    req(bet_data())
    ch <- .numeric_choices(bet_data())
    tagList(
      selectInput("bet_pp0", "2. Relative pressure (P/P0) column", choices = ch),
      selectInput("bet_q", "3. Quantity adsorbed (cm3/g STP) column", choices = ch)
    )
  })
  bet_result <- eventReactive(input$bet_run, {
    req(bet_data(), input$bet_pp0, input$bet_q)
    tryCatch(gui_bet(bet_data(), input$bet_pp0, input$bet_q),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(bet_result(), {
    r <- bet_result()
    store$bet <- data.frame(Qm_cm3g = r$Qm_cm3g, C = r$C, SBET_m2g = r$SBET_m2g, R2 = r$R2)
  })
  output$bet_table <- renderTable({
    req(bet_result()); r <- bet_result()
    data.frame(Qm_cm3g = r$Qm_cm3g, C = r$C, SBET_m2g = r$SBET_m2g, R2 = r$R2)
  })
  output$bet_download <- downloadHandler(
    filename = function() "bet_results.csv",
    content = function(file) {
      req(bet_result()); r <- bet_result()
      gui_write_csv(data.frame(Qm_cm3g = r$Qm_cm3g, C = r$C, SBET_m2g = r$SBET_m2g, R2 = r$R2), file)
    }
  )

  ## ---- TGA: curve analysis (DTG, stages, plot) ----
  tga_data <- mod_upload_server("tga")
  output$tga_col_ui <- renderUI({
    req(tga_data())
    ch <- .numeric_choices(tga_data())
    all_ch <- .all_choices(tga_data())
    base <- if (isTRUE(input$tga_batch)) {
      tagList(
        selectInput("tga_group", "2. Sample ID / group column", choices = all_ch),
        selectInput("tga_temp", "3. Temperature column (\u00b0C)", choices = ch),
        selectInput("tga_weight", "4. Weight % column", choices = ch)
      )
    } else {
      tagList(
        selectInput("tga_temp", "2. Temperature column (\u00b0C)", choices = ch),
        selectInput("tga_weight", "3. Weight % column", choices = ch)
      )
    }
    tagList(base,
            numericInput("tga_moisture_end", "Moisture-stage end (\u00b0C)", value = 110, min = 0),
            numericInput("tga_vm_end", "Volatile-matter-stage end (\u00b0C)", value = 650, min = 0))
  })
  tga_result <- eventReactive(input$tga_run, {
    req(tga_data(), input$tga_temp, input$tga_weight, input$tga_moisture_end, input$tga_vm_end)
    if (isTRUE(input$tga_batch)) {
      req(input$tga_group)
      stages <- tryCatch(
        withCallingHandlers(
          gui_tga_stages_batch(tga_data(), input$tga_group, input$tga_temp, input$tga_weight,
                                input$tga_moisture_end, input$tga_vm_end),
          warning = function(w) { showNotification(conditionMessage(w), type = "warning")
            invokeRestart("muffleWarning") }
        ),
        error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL }
      )
      req(stages)
      list(batch = TRUE, stages = stages, curve = NULL, dtg = NULL, peaks = NULL)
    } else {
      curve <- tryCatch(gui_tga_curve(tga_data(), input$tga_temp, input$tga_weight),
                         error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
      req(curve)
      stages <- tryCatch(
        withCallingHandlers(
          biocharkit::tga_stages(curve, moisture_end = input$tga_moisture_end, vm_end = input$tga_vm_end),
          warning = function(w) { showNotification(conditionMessage(w), type = "warning")
            invokeRestart("muffleWarning") }
        ),
        error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL }
      )
      dtg <- tryCatch(biocharkit::tga_dtg(curve),
                       error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
      peaks <- NULL
      if (!is.null(dtg)) {
        peaks <- biocharkit::find_dtg_peaks(dtg, min_prominence = if (is.null(input$tga_min_prom)) 0 else input$tga_min_prom)
        if (nrow(peaks) > 0) {
          assigned <- biocharkit::assign_dtg_peaks(peaks$peak_temperature_C)
          peaks$stage <- assigned$stage
          peaks$description <- assigned$description
        }
      }
      list(batch = FALSE, stages = stages, curve = curve, dtg = dtg, peaks = peaks)
    }
  })
  observeEvent(tga_result(), {
    r <- tga_result()
    plot_path <- if (!r$batch && !is.null(r$curve) && !is.null(r$dtg)) {
      gui_snapshot_png(biocharkit::plot_tga(r$curve, dtg = r$dtg))
    } else NULL
    store$tga <- list(batch = r$batch, stages = r$stages, peaks = r$peaks, plot_path = plot_path)
  })
  output$tga_stages_table <- renderTable({
    req(tga_result()); r <- tga_result(); req(!r$batch); r$stages
  })
  output$tga_batch_table <- DT::renderDataTable({
    req(tga_result()); r <- tga_result(); req(r$batch); r$stages
  }, options = list(pageLength = 10))
  output$tga_plot <- renderPlot({
    req(tga_result()); r <- tga_result(); req(!r$batch, r$curve, r$dtg)
    biocharkit::plot_tga(r$curve, dtg = r$dtg)
  })
  output$tga_peaks_table <- DT::renderDataTable({
    req(tga_result()); r <- tga_result(); req(!r$batch, r$peaks); r$peaks
  }, options = list(pageLength = 10))
  output$tga_download_csv <- downloadHandler(
    filename = function() "tga_proximate_analysis.csv",
    content = function(file) { req(tga_result()); r <- tga_result(); req(!r$batch); gui_write_csv(r$stages, file) }
  )
  output$tga_download_tiff <- downloadHandler(
    filename = function() "tga_dtg_plot.tif",
    content = function(file) {
      req(tga_result()); r <- tga_result(); req(!r$batch, r$curve, r$dtg)
      biocharkit::save_tiff(biocharkit::plot_tga(r$curve, dtg = r$dtg), filename = file)
    }
  )
  output$tga_batch_download <- downloadHandler(
    filename = function() "tga_proximate_analysis_batch.csv",
    content = function(file) { req(tga_result()); r <- tga_result(); req(r$batch); gui_write_csv(r$stages, file) }
  )

  ## ---- TGA: Kissinger kinetics (multi-heating-rate) ----
  tgakis_data <- mod_upload_server("tgakis")
  output$tgakis_col_ui <- renderUI({
    req(tgakis_data())
    ch <- .numeric_choices(tgakis_data())
    tagList(
      selectInput("tgakis_beta", "2. Heating rate column (\u00b0C/min)", choices = ch),
      selectInput("tgakis_tpeak", "3. DTG peak temperature column (\u00b0C)", choices = ch)
    )
  })
  tgakis_result <- eventReactive(input$tgakis_run, {
    req(tgakis_data(), input$tgakis_beta, input$tgakis_tpeak)
    tryCatch(gui_tga_kissinger(tgakis_data(), input$tgakis_beta, input$tgakis_tpeak),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(tgakis_result(), { store$tga_kissinger <- tgakis_result()$params })
  output$tgakis_table <- renderTable({ req(tgakis_result()); tgakis_result()$params })
  output$tgakis_download <- downloadHandler(
    filename = function() "tga_kissinger_kinetics.csv",
    content = function(file) { req(tgakis_result()); gui_write_csv(tgakis_result()$params, file) }
  )

  ## ---- Proximate / Ultimate Analysis ----
  prox_data <- mod_upload_server("prox")
  output$prox_col_ui <- renderUI({
    req(prox_data())
    ch <- .numeric_choices(prox_data())
    all_ch <- .all_choices(prox_data())
    tagList(
      selectInput("prox_moisture", "Moisture % column", choices = ch),
      selectInput("prox_vm", "Volatile matter % column", choices = ch),
      selectInput("prox_ash", "Ash % column", choices = ch),
      selectInput("prox_id", "(Optional) sample ID column", choices = c("(none)", all_ch))
    )
  })
  prox_result <- eventReactive(input$prox_run, {
    req(prox_data(), input$prox_moisture, input$prox_vm, input$prox_ash)
    idc <- if (is.null(input$prox_id) || input$prox_id == "(none)") NULL else input$prox_id
    tryCatch(gui_proximate(prox_data(), input$prox_moisture, input$prox_vm, input$prox_ash, idc),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(prox_result(), { store$proximate <- prox_result() })
  output$prox_table <- DT::renderDataTable({ req(prox_result()); prox_result() },
                                             options = list(pageLength = 10))
  output$prox_download <- downloadHandler(
    filename = function() "proximate_analysis.csv",
    content = function(file) { req(prox_result()); gui_write_csv(prox_result(), file) }
  )

  ult_data <- mod_upload_server("ult")
  output$ult_col_ui <- renderUI({
    req(ult_data())
    ch <- .numeric_choices(ult_data())
    all_ch <- .all_choices(ult_data())
    tagList(
      selectInput("ult_c", "C % column", choices = ch),
      selectInput("ult_h", "H % column", choices = ch),
      selectInput("ult_o", "O % column", choices = ch),
      selectInput("ult_n", "(Optional) N % column", choices = c("(none)", ch)),
      selectInput("ult_id", "(Optional) sample ID column", choices = c("(none)", all_ch))
    )
  })
  ult_result <- eventReactive(input$ult_run, {
    req(ult_data(), input$ult_c, input$ult_h, input$ult_o)
    nc <- if (is.null(input$ult_n) || input$ult_n == "(none)") NULL else input$ult_n
    idc <- if (is.null(input$ult_id) || input$ult_id == "(none)") NULL else input$ult_id
    tryCatch(gui_ultimate(ult_data(), input$ult_c, input$ult_h, input$ult_o, nc, idc),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(ult_result(), { store$ultimate <- ult_result() })
  output$ult_table <- DT::renderDataTable({ req(ult_result()); ult_result() },
                                            options = list(pageLength = 10))
  output$ult_download <- downloadHandler(
    filename = function() "ultimate_analysis.csv",
    content = function(file) { req(ult_result()); gui_write_csv(ult_result(), file) }
  )

  ## ---- Correlation Matrix ----
  corr_data <- mod_upload_server("corr")
  output$corr_col_ui <- renderUI({
    req(corr_data())
    checkboxGroupInput("corr_cols", "2. Select 2+ numeric columns to correlate",
                        choices = .numeric_choices(corr_data()))
  })
  corr_result <- eventReactive(input$corr_run, {
    req(corr_data(), input$corr_cols)
    tryCatch(gui_correlation(corr_data(), input$corr_cols, input$corr_method),
             error = function(e) { showNotification(conditionMessage(e), type = "error"); NULL })
  })
  observeEvent(corr_result(), { store$correlation <- corr_result()$long })
  output$corr_table <- DT::renderDataTable({ req(corr_result()); corr_result()$long },
                                             options = list(pageLength = 10))
  output$corr_download <- downloadHandler(
    filename = function() "correlation_results.csv",
    content = function(file) { req(corr_result()); gui_write_csv(corr_result()$long, file) }
  )

  ## ---- Combined report ----
  output$report_download <- downloadHandler(
    filename = function() paste0("biocharkit_session_report_", Sys.Date(), ".html"),
    content = function(file) {
      gui_render_report(reactiveValuesToList(store), file)
    }
  )
}

shinyApp(ui, server)
