## OpenRouter AI function
openrouter_ai <- function(
  image = NULL,
  docs = NULL,
  prompt = "Explain these images",
  model = "google/gemma-3-27b-it:free", # Nota: usa 27b, il 32b potrebbe avere ID diverso
  image_type = "png",
  retry_503 = 5,
  api_key = NULL,
  outputSize = "medium"
) {
  library(httr2)
  library(base64enc)

  # 1. Setup API Key
  if (is.null(api_key)) {
    api_key <- Sys.getenv("OPENROUTER_API_KEY")
  }

  # 2. Configurazione Tokens (Semplificata per evitare 400)
  # Molti modelli free falliscono se chiedi troppi token in uscita
  max_val <- switch(
    outputSize,
    "small" = 1000,
    "medium" = 2000,
    "large" = 4000,
    2000
  )

  # 3. Costruzione Content (Standard rigoroso OpenAI)
  content_list <- list(
    list(type = "text", text = prompt)
  )

  if (!is.null(image)) {
    for (img_path in as.vector(image)) {
      if (file.exists(img_path)) {
        # Importante: usiamo una compressione base64 pulita
        img_b64 <- base64encode(img_path)
        content_list <- append(
          content_list,
          list(
            list(
              type = "image_url",
              image_url = list(
                url = paste0("data:image/", image_type, ";base64,", img_b64)
              )
            )
          )
        )
      }
    }
  }

  # 4. Request Body
  # Rimosso 'seed' e altri parametri opzionali che possono causare 400 su certi provider
  request_body <- list(
    model = model,
    messages = list(
      list(role = "user", content = content_list)
    ),
    max_tokens = max_val,
    temperature = 0.7
  )

  # 5. Invio Richiesta
  url <- "https://openrouter.ai/api/v1/chat/completions"

  req <- request(url) |>
    req_headers(
      "Authorization" = paste("Bearer", api_key),
      "HTTP-Referer" = "https://github.com/massimoaria/bibliometrix", # Referer specifico
      "X-Title" = "Biblioshiny Client",
      "Content-Type" = "application/json"
    ) |>
    req_body_json(request_body) |>
    req_timeout(120) |>
    req_retry(max_tries = retry_503)

  resp <- req_perform(req) |>
    resp_body_json()

  if (!is.null(resp$choices)) {
    return(resp$choices[[1]]$message$content)
  } else {
    return(paste("❌ Errore API:", resp$error$message))
  }
}

## gemini AI tools
gemini_ai <- function(
  image = NULL,
  docs = NULL,
  prompt = "Explain these images",
  model = "2.5-flash",
  image_type = "png",
  retry_503 = 5,
  api_key = NULL,
  outputSize = "medium"
) {
  mime_doc_types <- list(
    pdf = "application/pdf",
    txt = "text/plain",
    html = "text/html",
    csv = "text/csv",
    rtf = "text/rtf"
  )

  switch(
    outputSize,
    "small" = {
      generation_config <- list(
        temperature = 1,
        maxOutputTokens = 8192,
        topP = 0.95,
        topK = 40,
        seed = 1234
      )
    },
    "medium" = {
      generation_config <- list(
        temperature = 1,
        maxOutputTokens = 16384, #8192,
        topP = 0.95,
        topK = 40,
        seed = 1234
      )
    },
    "large" = {
      generation_config <- list(
        temperature = 1,
        maxOutputTokens = 32768, #8192,
        topP = 0.95,
        topK = 40,
        seed = 1234
      )
    },
    "huge" = {
      generation_config <- list(
        temperature = 1,
        maxOutputTokens = 131072, #8192,
        topP = 0.95,
        topK = 40,
        seed = 1234
      )
    }
  )

  # # Default config
  # generation_config <- list(
  #   temperature = 1,
  #   maxOutputTokens = 16384,#8192,
  #   topP = 0.95,
  #   topK = 40,
  #   seed = 1234
  # )

  # Gemma models on the Gemini API: no "gemini-" prefix, no `seed`,
  # and a capped output window (open Gemma models max out at 8192 tokens).
  is_gemma <- startsWith(model, "gemma")
  if (is_gemma) {
    generation_config$seed <- NULL
    generation_config$maxOutputTokens <- min(
      generation_config$maxOutputTokens,
      8192
    )
  }

  # Build URL
  model_query <- if (is_gemma) {
    paste0(model, ":generateContent")
  } else {
    paste0("gemini-", model, ":generateContent")
  }
  url <- paste0(
    "https://generativelanguage.googleapis.com/v1beta/models/",
    model_query
  )
  if (is.null(api_key)) {
    api_key <- Sys.getenv("GEMINI_API_KEY")
  }

  # Base structure of parts
  parts <- list(list(text = prompt))

  # Handle images if provided
  if (!is.null(image)) {
    if (!is.vector(image)) {
      image <- as.vector(image)
    }
    mime_type <- paste0("image/", image_type)

    for (img_path in image) {
      if (!file.exists(img_path)) {
        return(paste0("❌ Error: Image file does not exist: ", img_path))
      }

      image_data <- tryCatch(
        base64enc::base64encode(img_path),
        error = function(e) {
          return(NULL)
        }
      )

      if (is.null(image_data)) {
        return(paste0("❌ Failed to encode image: ", img_path))
      }

      parts <- append(
        parts,
        list(
          list(
            inline_data = list(
              mime_type = mime_type,
              data = image_data
            )
          )
        )
      )
    }
  }

  # Handle documents if provided
  if (!is.null(docs)) {
    if (!is.vector(docs)) {
      docs <- as.vector(docs)
    }
    for (doc_path in docs) {
      if (!file.exists(doc_path)) {
        return(paste0("❌ Error: Document file does not exist: ", doc_path))
      }

      doc_data <- tryCatch(
        base64enc::base64encode(doc_path),
        error = function(e) {
          return(NULL)
        }
      )

      if (is.null(doc_data)) {
        return(paste0("❌ Failed to encode document: ", doc_path))
      }

      doc_type <- tools::file_ext(doc_path) |> tolower()

      if (doc_type %in% names(mime_doc_types)) {
        mime_type <- mime_doc_types[[doc_type]]
      } else {
        mime_type <- "application/pdf" # Default to PDF if unknown type
      }

      parts <- append(
        parts,
        list(
          list(
            inline_data = list(
              mime_type = "application/pdf",
              data = doc_data
            )
          )
        )
      )
    }
  }

  # Assemble request body
  request_body <- list(
    contents = list(
      parts = parts
    ),
    generationConfig = generation_config
  )

  # Helper: pull the real error message out of a Gemini error response body,
  # returning NULL when the body can't be parsed or carries no message.
  extract_api_error <- function(resp) {
    tryCatch(
      {
        parsed <- jsonlite::fromJSON(httr2::resp_body_string(resp))
        msg <- parsed$error$message
        if (is.null(msg) || !nzchar(msg)) NULL else msg
      },
      error = function(e) NULL
    )
  }

  # Helper: parse a Gemini 429 body into the human message, the server-suggested
  # retry delay (seconds, from the RetryInfo detail) and whether the violated
  # quota is a per-DAY cap — which a retry cannot clear, unlike a per-minute one.
  parse_api_error <- function(resp) {
    out <- list(message = NULL, retry_delay = NA_real_, is_daily = FALSE)
    tryCatch(
      {
        err <- jsonlite::fromJSON(
          httr2::resp_body_string(resp),
          simplifyVector = FALSE
        )$error
        if (!is.null(err$message) && nzchar(err$message)) {
          out$message <- err$message
        }
        for (d in err$details) {
          dtype <- if (is.null(d[["@type"]])) "" else d[["@type"]]
          if (grepl("RetryInfo", dtype) && !is.null(d$retryDelay)) {
            out$retry_delay <- suppressWarnings(
              as.numeric(sub("s$", "", d$retryDelay))
            )
          }
          if (grepl("QuotaFailure", dtype)) {
            ids <- vapply(d$violations, function(v) {
              if (is.null(v$quotaId)) "" else v$quotaId
            }, character(1))
            if (any(grepl("PerDay", ids, ignore.case = TRUE))) {
              out$is_daily <- TRUE
            }
          }
        }
        out
      },
      error = function(e) out
    )
  }

  # Retry loop
  for (attempt in seq_len(retry_503)) {
    # Build and send request. Disable httr2's default "throw on HTTP error
    # status" so 4xx/5xx come back as normal response objects: this lets us
    # read the actual error message Gemini returns instead of masking every
    # failure as an invalid API key.
    req <- request(url) |>
      req_url_query(key = api_key) |>
      req_headers("Content-Type" = "application/json") |>
      req_body_json(request_body) |>
      req_timeout(120) |>
      req_error(is_error = function(resp) FALSE)

    resp <- tryCatch(
      req_perform(req),
      error = function(e) {
        # Transport-level failure (timeout, DNS, no network) — not an HTTP
        # status. Flag it so it is handled separately from HTTP errors.
        structure(
          list(connection_error = TRUE, message = e$message),
          class = "gemini_conn_error"
        )
      }
    )

    # Connection-level error: retry if attempts remain, otherwise report it.
    if (inherits(resp, "gemini_conn_error")) {
      if (attempt < retry_503) {
        message(paste0(
          "⚠️ Connection error - retrying (attempt ",
          attempt, "/", retry_503, ")..."
        ))
        Sys.sleep(min(2^attempt, 16))
        next
      }
      return(paste0(
        "❌ Request failed (connection error): ", resp$message,
        "\nPlease check your internet connection and try again."
      ))
    }

    # 429 = RESOURCE_EXHAUSTED: a quota / rate-limit response, NOT server
    # overload. On a paid tier this is typically the per-MINUTE request cap
    # (RPM) hit by a burst — low-RPM models such as Gemini Pro (~25 RPM) trip
    # far sooner than Flash (1000+ RPM) — or shared-capacity throttling on the
    # newest flagship models. Gemini reports how long to wait (RetryInfo) and
    # whether the cap is per-day (a retry cannot clear that), so honour both
    # instead of hammering with a fixed backoff and masking the real cause.
    if (resp$status_code == 429) {
      err <- parse_api_error(resp)
      msg <- err$message

      # Depleted prepayment / billing credits: a hard 429 that a retry cannot
      # clear. The paid "Pro" models (e.g. gemini-2.5-pro, gemini-3.1-pro)
      # draw on prepay credits; when those run out Google returns this even
      # though Flash models on the same key keep working.
      is_billing <- !is.null(msg) && grepl(
        "prepay|credits? (are )?depleted|billing|insufficient",
        msg,
        ignore.case = TRUE
      )
      if (is_billing) {
        return(paste0(
          "❌ HTTP 429: ",
          paste0(msg, "\n\n"),
          "This model needs available billing/prepayment credits on the key's ",
          "Google Cloud project — retrying will not help. Top up or enable ",
          "billing at https://ai.studio/projects , or switch to a Flash model ",
          "(e.g. Gemini 2.5 Flash), which is often covered separately. Note ",
          "that restricted promotional/EDU credits may not apply to the Pro ",
          "models' prepay balance."
        ))
      }

      # Per-day quota exhausted: retrying is futile — report and stop.
      if (isTRUE(err$is_daily)) {
        return(paste0(
          "❌ HTTP 429: ",
          if (!is.null(msg)) paste0(msg, "\n\n") else "",
          "You have reached the per-DAY request quota for this model. ",
          "Retrying will not help until it resets (around midnight Pacific ",
          "time). Select a different model or try again tomorrow."
        ))
      }

      # Per-minute / capacity throttling: wait the server-suggested delay
      # (capped) and retry while attempts remain.
      if (attempt < retry_503) {
        wait <- if (!is.na(err$retry_delay) && err$retry_delay > 0) {
          min(err$retry_delay + 1, 35)
        } else {
          min(2^attempt, 16)
        }
        message(paste0(
          "⚠️ HTTP 429 (rate limited) - waiting ", round(wait),
          "s before retry (attempt ", attempt, "/", retry_503, ")..."
        ))
        Sys.sleep(wait)
        next
      }

      return(paste0(
        "❌ HTTP 429: ",
        if (!is.null(err$message)) paste0(err$message, "\n\n") else "",
        "This is a per-minute rate-limit (RPM) or shared-capacity response, ",
        "not a server outage. The request was retried ", retry_503,
        " times without success. Try a higher-throughput model such as ",
        "Gemini 2.5 Flash (much higher RPM than the Pro models), run the ",
        "analysis less frequently, or wait a minute and retry."
      ))
    }

    # Retry on transient server errors (5xx).
    if (resp$status_code %in% c(500, 502, 503, 504)) {
      if (attempt < retry_503) {
        message(paste0(
          "⚠️ HTTP ", resp$status_code,
          " (transient server error) - retrying (attempt ",
          attempt,
          "/",
          retry_503,
          ")..."
        ))
        Sys.sleep(min(2^attempt, 16))
        next
      } else {
        return(
          paste0(
            "❌ HTTP ", resp$status_code,
            ": the AI service is temporarily unavailable.\n",
            "The Google Gemini servers are currently overloaded or unstable.\n",
            "All retry attempts failed (",
            retry_503,
            "). Please try again in a few minutes, or select a different AI model."
          )
        )
      }
    }

    # HTTP errors
    # 400 api key not valid
    if (resp$status_code == 400) {
      msg <- tryCatch(
        {
          parsed <- jsonlite::fromJSON(httr2::resp_body_string(resp))
          parsed$error$message
        },
        error = function(e) {
          "Please check your API key. It seems to be not valid!"
        }
      )
      return(paste0("❌ HTTP ", resp$status_code, ": ", msg))
    }
    # Other HTTP errors
    if (resp$status_code != 200) {
      msg <- tryCatch(
        {
          parsed <- jsonlite::fromJSON(httr2::resp_body_string(resp))
          parsed$error$message
        },
        error = function(e) {
          "Service unavailable or unexpected error. Please check your API key and usage limit."
        }
      )

      return(paste0("❌ HTTP ", resp$status_code, ": ", msg))
    }

    # Successful response — concatenate answer text, skipping any "thought"
    # parts (thinking models such as Gemma 4 return their reasoning in a
    # separate part flagged with thought = TRUE).
    candidates <- httr2::resp_body_json(resp)$candidates
    answer <- unlist(lapply(candidates, function(cand) {
      lapply(cand$content$parts, function(p) {
        if (isTRUE(p$thought)) NULL else p$text
      })
    }))
    answer <- answer[!is.na(answer) & nzchar(answer)]
    if (length(answer) == 0) {
      return(paste0(
        "❌ The model returned an empty response. ",
        "Please try again or select a different AI model."
      ))
    }
    return(paste(answer, collapse = "\n"))
  }
}


setGeminiAPI <- function(api_key) {
  # 1. Controllo validità dell'API key
  apiCheck <- gemini_ai(
    image = NULL,
    prompt = "Hello",
    model = "2.5-flash",
    image_type = "png",
    retry_503 = 5,
    api_key = api_key
  )

  contains_http_error <- grepl("HTTP\\s*[1-5][0-9]{2}", apiCheck)

  if (contains_http_error) {
    return(list(
      valid = FALSE,
      message = "❌ API key seems be not valid! Please, check it or your connection."
    ))
  }

  if (is.null(api_key) || !is.character(api_key) || nchar(api_key) == 0) {
    return(list(
      valid = FALSE,
      message = "❌ API key must be a non-empty string."
    ))
  }

  if (nchar(api_key) < 10) {
    return(list(
      valid = FALSE,
      message = "❌ API key seems too short. Please verify your key."
    ))
  }

  # 2. Mostra solo gli ultimi 4 caratteri per feedback
  last_chars <- 4
  last <- substr(
    api_key,
    max(1, nchar(api_key) - last_chars + 1),
    nchar(api_key)
  )

  # 3. Imposta la variabile d'ambiente
  Sys.setenv(GEMINI_API_KEY = api_key)

  return(list(
    valid = TRUE,
    message = paste0(
      paste0(rep("*", nchar(api_key) - 4), collapse = ""),
      last,
      collapse = ""
    )
  ))
}

showGeminiAPI <- function() {
  api_key <- Sys.getenv("GEMINI_API_KEY")
  last_chars <- 4
  last <- substr(
    api_key,
    max(1, nchar(api_key) - last_chars + 1),
    nchar(api_key)
  )
  last <- paste0(
    paste0(rep("*", nchar(api_key) - 4), collapse = ""),
    last,
    collapse = ""
  )
  return(last)
}

load_api_key <- function(path = path_gemini_key) {
  if (file.exists(path)) {
    key <- readLines(path, warn = FALSE)
    if (nchar(key) >= 10) {
      Sys.setenv(GEMINI_API_KEY = key)
      return(TRUE)
    }
  }
  return(FALSE)
}

loadGeminiModel = function(file) {
  # load info about model type and output size
  if (file.exists(file)) {
    model <- readLines(file, warn = FALSE)
  } else {
    model <- c("2.5-flash-lite", "medium")
  }
  if (length(model == 1)) {
    model <- c(model, "medium")
  }
  return(model)
}

saveGeminiModel = function(model, file) {
  if (file.exists(file)) {
    file.remove(file)
  }
  writeLines(model, file)
}

geminiOutput <- function(title = "", content = "", values) {
  if (is.null(content)) {
    content <- "Click the 'Ask Biblio AI' button to analyze the visual outputs and provide an automatic interpretation of your results based on the graphs.\n
This AI-powered feature leverages Google Gemini to help you understand patterns and insights emerging from your contextual analysis.\n  \n  \n \n  \n  \n  \n"
  }
  box(
    title = title,
    width = 12,
    status = "info",
    solidHeader = TRUE,
    div(
      id = "typing-box",
      style = "white-space: pre-wrap; background-color:#f9f9f9; padding:15px; border:1px solid #ccc; border-radius:5px; max-height:400px; overflow-y: auto;",
      #HTML(text_to_html(content))
      HTML(gemini_to_html(content))
    ),
    br(),
    em(
      "You can modify or enrich the proposed prompt with additional context or details about your analysis to help 'Biblio AI' generate a more accurate and meaningful interpretation."
    ),
    textAreaInput(
      inputId = "gemini_additional",
      label = NULL,
      value = values$gemini_model_parameters,
      placeholder = "You can provide additional context or details about your analysis to help 'Biblio AI' generate a more accurate and meaningful interpretation.",
      rows = 3,
      width = "100%"
    ),
    fluidRow(
      column(
        4,
        align = "center",
        actionButton(
          "gemini_btn",
          "Ask Biblio AI",
          style = "color: white;",
          icon(name = "microchip", lib = "font-awesome"),
          width = "80%"
        )
      ),
      column(
        4,
        align = "center",
        actionButton(
          "copy_btn",
          "Copy",
          style = "color: white;",
          icon = icon("clipboard"),
          width = "80%"
        )
      ),
      column(
        4,
        align = "center",
        downloadButton(
          outputId = "save_btn",
          label = "Save",
          icon = icon("download"),
          style = "width: 80%;"
        )
      )
    )
  )
}

biblioAiPrompts <- function(values, activeTab) {
  ## ROle definition for Gemini as Biblio AI assistant
  promptInitial <- "You are Biblio AI, an AI assistant integrated within Biblioshiny. Your task is to support researchers in interpreting and critically discussing the results of their bibliometric analyses, offering insights, contextual explanations, and guidance for data-driven interpretation. "

  switch(
    activeTab,
    "mainInfo" = {
      prompt <- paste0(
        "Provide an interpretation of these statistics summarizing the bibliographic collection. ",
        "Focus on key metrics such as the number of documents, sources, authors, and citations, ",
        "and discuss what they reveal about the scope, productivity, and impact of the collection. ",
        "This is the list of statistics: ",
        merge_df_to_string(values$TABvb)
      )
    },
    "lifeCycle" = {
      prompt <- paste0(
        "Analyze the provided 'life cycle' plots showing the bibliographic collection's scientific production fitted to a logistic growth model. ",
        "You have been provided with: (1) an ANNUAL plot showing yearly publication counts, (2) a CUMULATIVE plot showing the total publications over time, ",
        "and (3) the logistic model parameters and goodness-of-fit metrics.\n\n",

        "Please provide a comprehensive interpretation covering:\n",
        "1. GROWTH PATTERN: Describe the overall growth trajectory (exponential, linear, or saturation phase). ",
        "Identify the inflection point where growth rate changes.\n",
        "2. MODEL FIT: Evaluate how well the logistic curve fits the observed data based on the provided metrics (R², RMSE, etc.). ",
        "Comment on any systematic deviations.\n",
        "3. KEY PARAMETERS: Interpret the logistic model parameters:\n",
        "   - K (carrying capacity): the estimated maximum cumulative production\n",
        "   - r (growth rate): the intrinsic rate of increase\n",
        "   - t₀ (inflection point): when maximum growth rate occurred\n",
        "4. TRENDS AND PHASES: Identify distinct phases in the publication timeline:\n",
        "   - Emergence phase (slow initial growth)\n",
        "   - Expansion phase (rapid growth)\n",
        "   - Maturity/saturation phase (if present)\n",
        "5. SCIENTIFIC IMPLICATIONS: Discuss what these patterns suggest about the research field's maturity, ",
        "potential factors influencing growth (e.g., funding, technological advances, paradigm shifts), ",
        "and future trajectories. Is the field still growing or approaching saturation?\n\n",

        "Provide your analysis in a structured format with clear sections.",
        "This a the list of statistics: Parameters ",
        merge_df_to_string(values$DLC$parameters),
        ", parameters_real_years ",
        merge_df_to_string(values$DLC$parameters_real_years),
        ", and goodness_of_fit ",
        merge_df_to_string(values$DLC$metrics)
      )
    },
    "threeFieldPlot" = {
      prompt <- paste0(
        "Provide an interpretation of this Three-Field Plot. The central field represents the target metadata,",
        " while the left and the right fields are the destination metadata. The plot visualizes the connections",
        " among these three dimensions by linking the target field witgh the two other metadata.",
        " Analyze the structure of the network, and describe how these elements are interconnected."
      )
    },
    "authorsProdOverTime" = {
      docs <- apot2Docs(values$AUProdOverTime, n = 3) %>% merge_df_to_string()
      prompt <- paste0(
        "Provide an interpretation of this 'Authors’ Production Over Time' plot, which displays bibliometric trends",
        " for the top authors in the field. For each author, the red horizontal line represents their scientific timeline,",
        " indicating the span of their active publishing years. Each bubble corresponds to a publication year: its size reflects",
        " the number of articles published that year, while the color intensity represents the total number of citations per year (TC/year).",
        " Analyze the evolution of individual authors’ productivity and impact, identifying key periods of activity, citation peaks, ",
        "and possible shifts in research influence over time. A list of the most three cited-per-year articles for each author is provided: ",
        docs
      )
    },
    "correspAuthorCountry" = {
      countries <- merge_df_to_string(country2collab(values$TABCo))
      prompt <- paste0(
        "Provide an interpretation of this 'Corresponding Author’s Country Collaboration Plot'. The plot displays the number of scientific publications",
        " by corresponding authors for each country, distinguishing between Single Country Publications (SCP) and Multiple Country Publications (MCP). ",
        "SCP represents articles authored exclusively within one country, while MCP refers to collaborative works involving authors from different countries. ",
        "The MCP Ratio, calculated as MCP divided by the total number of publications, indicates the extent of international collaboration. ",
        "Focus on identifying the most productive countries, comparing their levels of international collaboration, and discussing the balance between domestic ",
        "and global research engagement. A list of the most 20 frequent countries is provided: ",
        countries
      )
    },
    "mostLocalCitDoc" = {
      docs <- localCit2docs(values$TABLocDoc, n = 20) %>% merge_df_to_string()
      prompt <- paste0(
        "Provide an interpretation of this table reporting the most locally and globally cited articles in the collection. Global Citations (GC) represent the total",
        " number of citations a document has received across all records indexed in the bibliographic database, indicating its overall scholarly impact. Local Citations (LC),",
        " on the other hand, count the number of citations a document has received within this specific dataset, reflecting its relevance to the focused research field. ",
        "Both indicators are normalized by NTC (Normalized Total Citations), which adjusts citation counts relative to the average number of citations for publications from the same year.",
        " This normalization allows for fair comparison across time. Focus on identifying articles with strong global influence, local relevance, or both.",
        "A list of the most 20 local cited articles is provided: ",
        docs
      )
    },
    "trendTopic" = {
      prompt <- paste0(
        "Provide an interpretation of this trend topics plot, where each year is associated with the k words that have the highest annual median frequency. ",
        "For each word, the light blue line represents the interquartile range (from the first to the third quartile) of its frequency time series, and the central",
        "point marks the median value. The size of the bubbles is proportional to the annual frequency, reflecting the relative prominence of each term over time. "
      )
    },
    "ReferenceSpect" = {
      references <- merge_df_to_string(values$res$peaks)
      sequenceTop5 <- values$res$Sequences %>%
        filter(Class != "") %>%
        group_by(Class) %>%
        slice_max(Freq, n = 5) %>%
        rename("Totale Citation" = "Freq")
      prompt <- paste0(
        "Provide an interpretation of this Reference Publication Year Spectroscopy plot (RPYS, Marx et al. 2014, JAIST).",
        " The black line shows the number of cited references by publication year. ",
        "The red line represents the deviation from the 5-year median citation frequency.",
        " This highlights peak years of historical significance. A list of the most cited references is provided for the first ten peak years identified by the red line,",
        " along with their citation frequencies: ",
        references,
        "\nIn addition, please highlight which references are the most important according to the following categories: Hot Paper, Life Cycle, Sleeping Beauty,",
        "
                         and Constant Performer (Thor A. et al. 2018, Identifying single influential publications in a research field: new analysis opportunities of the CRExplorer. Scientometrics).",
        " Below is the list of references with their assigned category.",
        sequenceTop5
      )
    },
    "coOccurenceNetwork" = {
      prompt <- paste0(
        "Provide an interpretation of this 'word co-occurrence' network.",
        " Focus on the structure of the network, the presence of communities (topics), and the relevance of the most connected terms."
      )
      if (!is.null(values$cocnet$doc2clust)) {
        docs <- merge_df_to_string(doc2clust(values$cocnet))
        prompt <- paste0(
          prompt,
          " Here there is the list of three most central articles for each cluster: ",
          docs,
          ". Also describe the articles belonging to each community."
        )
      }
    },
    "thematicMap" = {
      docs <- merge_df_to_string(doc2clust(values$TM))
      prompt <- paste0(
        "Provide an interpretation of this 'strategic map'",
        "Here there is the list of three most central articles for each cluster: ",
        docs,
        ". Focus on the structure of the strategic map, the description of clusters and their centrality and density measures, and the description of the articles belonging to them."
      )
    },
    "thematicEvolution" = {
      prompt <- paste0(
        "Provide an interpretation of these plots.",
        "The first plot illustrates topic evolution across different time periods, showing how thematic areas develop or shift over time. ",
        "The other plots display strategic maps for each period, highlighting the positioning of topics based on centrality and density. ",
        "Focus on identifying emerging, declining, or stable topics and their strategic importance."
      )
    },
    "factorialAnalysis" = {
      prompt <- "Provide an interpretation of this 'factorial map'. Focus on the structure of the map, the presence of clusters, and the relevance of the most contributing terms."
    },
    "coCitationNetwork" = {
      prompt <- "Provide an interpretation of this 'co-citation' network. Focus on the structure of the network, the presence of communities, and the relevance of the most connected terms."
    },
    "historiograph" = {
      titles <- hist2docs(values$histPlotVis$VIS$x$nodes) %>%
        merge_df_to_string()
      prompt <- paste0(
        "Interpret this historiograph, a temporal citation network built by mapping direct citation links among documents. ",
        "Highlight the main citation paths, pivotal works, and any notable temporal trends in knowledge development, looking also to the article titles and their topics.",
        " Focus on the temporal evolution of each cluster",
        "Here there is the list of each paper (node) and its title: ",
        titles
      )
    },
    "collabNetwork" = {
      prompt <- paste0(
        "Provide an interpretation of this 'collaboration' network",
        ". Focus on the structure of the network, the presence of communities, and the relevance of the most connected terms."
      )
    },
    "collabWorldMap" = {
      #values$WMGemini
      prompt <- paste0(
        "Provide an interpretation of this ‘Countries’ Collaboration World Map’. The map visualizes international scientific ",
        "collaboration by showing, for each country, the total number of articles with at least one contributing author. ",
        "The color intensity of each country is proportional to its research output. The connecting lines represent collaborative ",
        "links between countries, based on co-authorship across all authors (not only corresponding authors). ",
        "Focus on identifying major hubs of scientific production, key international partnerships, and global patterns of collaboration."
      )
    },
    "bradford" = {
      prompt <- paste0(
        "Provide an interpretation of this Bradford bibliograph and Bradford’s Law analysis. ",
        "The plot shows the cumulative number of articles (y-axis) against the logarithm of source rank (x-axis). ",
        "The solid black line is the empirical curve and the dashed red line is the theoretical fitted Bradford distribution: C(r) = a + b * log(r). ",
        "The three shaded zones represent the Bradford zones: Zone 1 (core journals), Zone 2 (intermediate), and Zone 3 (periphery). ",
        "Each zone contains approximately one-third of the total articles, but the number of journals increases geometrically (ratio 1:k:k^2). ",
        "Focus on: (1) the degree of concentration vs dispersion in the field’s publication landscape, ",
        "(2) the shape of the curve (steep initial drop = high concentration, flatter = more dispersion), ",
        "(3) how well the empirical data fits the theoretical Bradford distribution (R-squared and KS test), ",
        "(4) the Bradford multiplier k and what it reveals about the zone structure, ",
        "(5) identifying and discussing the core journals and their role in the field."
      )
    },
    "lotka" = {
      prompt <- paste0(
        "Provide an interpretation of this Lotka’s Law plot and analysis. ",
        "The plot shows the author productivity distribution: the percentage of authors (y-axis) who published a given number of documents (x-axis). ",
        "Three curves are displayed: (1) the solid black line is the empirical distribution, ",
        "(2) the dashed red line is the theoretical Lotka’s Law with Beta=2 (inverse square law), ",
        "(3) the dot-dash blue line is the fitted generalized inverse power law with the estimated Beta. ",
        "Lotka’s Law states that f(n) = C / n^Beta, where f(n) is the proportion of authors publishing n articles. ",
        "Focus on: (1) how well the empirical data fits the theoretical (Beta=2) and fitted distributions (KS test results), ",
        "(2) the estimated Beta value and what it reveals about productivity concentration in the field, ",
        "(3) the proportion of single-paper authors vs prolific authors, ",
        "(4) whether Beta > 2 (more concentrated, typical of niche fields) or Beta < 2 (more dispersed, typical of broad fields), ",
        "(5) implications for the field’s author productivity patterns."
      )
    },
    {
      prompt <- paste0(
        "Provide an interpretation of this plot creted with ‘bibliometrix R Package’"
      )
    }
  )
  prompt <- paste0(promptInitial, prompt)
  #if (!activeTab %in% c("mainInfo", "thematicMap", "trendTopic")) prompt <- paste0(prompt, " Provide also scientific references about the methodological description")
  return(prompt)
}


geminiParameterPrompt <- function(values, activeTab, input) {
  if ("DB_Original" %in% names(values$M)) {
    DB <- unique(values$M$DB_Original)
    if (length(DB) > 1) {
      DB[length(DB)] <- paste0("and ", DB[length(DB)])
    }
    DB <- gsub("ISI", "WOS", paste0(DB, collapse = ", "))

    DB <- paste0(
      "databases ",
      DB,
      " merged by Bibliometrix R package routines."
    )
  } else {
    if (values$M$DB[1] == "ISI") {
      DB <- "WOS"
    } else {
      DB <- values$M$DB[1]
    }
    DB <- paste0("database ", DB, ".")
  }
  txt <- paste0(
    "The analysis was performed on a collection downloaded from the following bibliographic ",
    DB
  )
  missingTags <- values$missingdf %>%
    filter(status %in% "Completely missing") %>%
    pull(description)
  if (length(missingTags) > 0) {
    if ("DB_Original" %in% names(values$M)) {
      txt1 <- paste0(
        " Some metadata fields like ",
        paste0(missingTags, collapse = ", "),
        " are missing due to limitations in the merging process and 
                                                           non-standard formats across databases."
      )
      txt <- paste0(txt, txt1)
    }
  }

  switch(
    activeTab,
    "mainInfo" = {
      req(values$TABvb)
    },
    "lifeCycle" = {
      req(values$DLC)
    },
    "threeFieldPlot" = {
      req(values$TFP)
      txt <- paste0(
        txt,
        " This graph was generated with the following parameters: target field '",
        input$CentralField,
        "', right field '",
        input$RightField,
        "', and left field '",
        input$LeftField,
        "'."
      )
    },
    "trendTopic" = {
      req(values$trendTopics)
      txt <- paste0(
        txt,
        " This graph was generated with the following parameters: ",
        merge_df_to_string(values$trendTopics$params)
      )
    },
    "authorsProdOverTime" = {
      req(values$AUProdOverTime)
    },
    "correspAuthorCountry" = {
      req(values$TABCo)
    },
    "mostLocalCitDoc" = {
      req(values$TABLocDoc)
    },
    "ReferenceSpect" = {
      req(values$res)
    },
    "coOccurenceNetwork" = {
      req(values$COCnetwork)
      txt <- paste0(
        txt,
        " This graph was generated with the following parameters: ",
        merge_df_to_string(values$cocnet$params)
      )
    },
    "thematicMap" = {
      req(values$TMmap)
      txt <- paste0(
        txt,
        "  This graph was generated with the following parameters: ",
        merge_df_to_string(values$TM$params)
      )
    },
    "thematicEvolution" = {
      req(values$nexus)
      txt <- paste0(
        txt,
        " The following parameters were used for each strategic map: ",
        merge_df_to_string(values$nexus$TM[[1]]$params)
      )
    },
    "factorialAnalysis" = {
      req(values$plotCS)
      txt <- paste0(
        txt,
        "  This graph was generated with the following parameters: ",
        merge_df_to_string(values$CS$params)
      )
    },
    "coCitationNetwork" = {
      req(values$COCITnetwork)
      txt <- paste0(
        txt,
        "  This graph was generated with the following parameters: ",
        merge_df_to_string(values$cocitnet$params)
      )
    },
    "historiograph" = {
      req(values$histPlotVis$VIS)
    },
    "collabNetwork" = {
      req(values$COLnetwork$VIS)
      txt <- paste0(
        txt,
        "  This graph was generated with the following parameters: ",
        merge_df_to_string(values$colnet$params)
      )
    },
    "collabWorldMap" = {
      req(values$WMmap)
    },
    "bradford" = {
      req(values$bradford)
      s <- values$bradford$stat
      zs <- values$bradford$zoneSummary
      txt <- paste0(
        txt,
        " Bradford's Law analysis results: ",
        "Fitted model C(r) = ", round(s$a, 2), " + ", round(s$b, 2), " * log(r), ",
        "R-squared = ", round(s$R2, 4), ", ",
        "Bradford multiplier k = ", round(s$k, 2), ", ",
        "KS test p-value = ", format.pval(s$ks.pvalue, digits = 3), ". ",
        "Zone summary: ", merge_df_to_string(zs)
      )
    },
    "lotka" = {
      req(values$lotka)
      s <- values$lotka$stat
      txt <- paste0(
        txt,
        " Lotka's Law analysis results: ",
        "Estimated Beta = ", round(s$Beta, 4), ", ",
        "Constant C = ", round(s$C, 4), ", ",
        "R-squared = ", round(s$R2, 4), ", ",
        "KS p-value (theoretical Beta=2) = ", format.pval(s$ks.theo.pvalue, digits = 3), ", ",
        "KS p-value (fitted Beta) = ", format.pval(s$ks.fit.pvalue, digits = 3), ". ",
        "Author productivity table: ", merge_df_to_string(values$lotka$AuthorProd)
      )
    }
  )
  return(txt)
}



geminiWaitingMessage <- function(values, activeTab) {
  messageTxt <- "⌛ Thinking..."

  switch(
    activeTab,
    "mainInfo" = {
      req(values$TABvb)
      values$MainInfoGemini <- messageTxt
    },
    "lifeCycle" = {
      req(values$DLC)
      values$DLCGemini <- messageTxt
    },
    "threeFieldPlot" = {
      req(values$TFP)
      values$TFPGemini <- messageTxt
    },
    "authorsProdOverTime" = {
      req(values$AUProdOverTime)
      values$ApotGemini <- messageTxt
    },
    "correspAuthorCountry" = {
      req(values$TABCo)
      values$MostRelCountriesGemini <- messageTxt
    },
    "mostLocalCitDoc" = {
      req(values$TABLocDoc)
      values$MostLocCitDocsGemini <- messageTxt
    },
    "trendTopic" = {
      req(values$trendTopics)
      values$trendTopicsGemini <- messageTxt
    },
    "ReferenceSpect" = {
      req(values$res)
      values$rpysGemini <- messageTxt
    },
    "coOccurenceNetwork" = {
      req(values$COCnetwork)
      values$cocGemini <- messageTxt
    },
    "thematicMap" = {
      req(values$TM)
      values$TMGemini <- messageTxt
    },
    "thematicEvolution" = {
      req(values$nexus)
      values$TEGemini <- messageTxt
    },
    "factorialAnalysis" = {
      req(values$plotCS)
      values$CSGemini <- messageTxt
    },
    "coCitationNetwork" = {
      req(values$COCITnetwork)
      values$cocitGemini <- messageTxt
    },
    "historiograph" = {
      req(values$histPlotVis$VIS)
      values$histGemini <- messageTxt
    },
    "collabNetwork" = {
      req(values$COLnetwork$VIS)
      values$colGemini <- messageTxt
    },
    "collabWorldMap" = {
      req(values$WMmap)
      values$WMGemini <- messageTxt
    },
    "bradford" = {
      req(values$bradford)
      values$BradfordGemini <- messageTxt
    },
    "lotka" = {
      req(values$lotka)
      values$LotkaGemini <- messageTxt
    }
  )
  return(values)
}

## Map activeTab to reactive field name in values
geminiFieldName <- function(activeTab) {
  switch(
    activeTab,
    "mainInfo" = "MainInfoGemini",
    "lifeCycle" = "DLCGemini",
    "threeFieldPlot" = "TFPGemini",
    "authorsProdOverTime" = "ApotGemini",
    "correspAuthorCountry" = "MostRelCountriesGemini",
    "mostLocalCitDoc" = "MostLocCitDocsGemini",
    "trendTopic" = "trendTopicsGemini",
    "ReferenceSpect" = "rpysGemini",
    "coOccurenceNetwork" = "cocGemini",
    "thematicMap" = "TMGemini",
    "thematicEvolution" = "TEGemini",
    "factorialAnalysis" = "CSGemini",
    "coCitationNetwork" = "cocitGemini",
    "historiograph" = "histGemini",
    "collabNetwork" = "colGemini",
    "collabWorldMap" = "WMGemini",
    "bradford" = "BradfordGemini",
    "lotka" = "LotkaGemini",
    NULL
  )
}

## Prepare image files for Gemini (synchronous part of geminiPromptImage)
geminiPrepareImage <- function(obj, type, prompt, key, desc, values) {
  # Check internet connection
  if (!is_online()) {
    return(list(
      prompt = NULL,
      image_paths = NULL,
      error_msg = '⚠️ **Note**: Biblio AI requires an active internet connection to work.'
    ))
  }
  # Check Chrome browser
  if (is.null(values$Chrome_url)) {
    return(list(
      prompt = NULL,
      image_paths = NULL,
      error_msg = '⚠️ **Note**: Biblio AI requires a **Chrome-based browser** (such as Google Chrome or Microsoft Edge) installed on your computer to work correctly.'
    ))
  }

  if (!key) {
    return(list(
      prompt = NULL,
      image_paths = NULL,
      error_msg = 'To access this feature, please provide a valid Gemini AI API key. You can obtain your API key by visiting the official <a href="https://aistudio.google.com/" target="_blank">Google AI Studio website</a>.'
    ))
  }

  # Build prompt with description
  if (!is.null(desc)) {
    prompt <- paste0(prompt, desc, collapse = ". ")
  }

  # Convert plot to PNG
  file_path <- tempfile(fileext = ".png", tmpdir = getWD())

  result <- tryCatch({
    switch(
      type,
      "vis" = {
        plot2pngGemini(obj, filename = file_path, type = "vis")
      },
      "plotly" = {
        plot2pngGemini(obj, filename = file_path, type = "plotly")
      },
      "multi" = {
        file_path <- obj
      },
      "text" = {
        file_path <- NULL
      },
      "ggplot2" = {
        ggsave(
          filename = file_path,
          plot = obj,
          dpi = 72,
          height = 7,
          width = 14,
          bg = "transparent"
        )
      }
    )
    NULL  # no error
  }, error = function(e) {
    conditionMessage(e)
  })

  if (!is.null(result)) {
    return(list(
      prompt = NULL,
      image_paths = NULL,
      error_msg = paste("Error preparing image:", result)
    ))
  }

  return(list(
    prompt = prompt,
    image_paths = file_path,
    error_msg = NULL
  ))
}

## Cleanup temporary files after Gemini API call
geminiCleanupFiles <- function(image_paths) {
  if (!is.null(image_paths[1])) {
    tryCatch({
      filename_html <- paste0(tools::file_path_sans_ext(image_paths), ".html")
      existing <- c(image_paths, filename_html)
      existing <- existing[file.exists(existing)]
      if (length(existing) > 0) file.remove(existing)
      dirs <- paste0(tools::file_path_sans_ext(image_paths), "_files")
      dirs <- dirs[dir.exists(dirs)]
      if (length(dirs) > 0) unlink(dirs, recursive = TRUE)
    }, error = function(e) {
      # Silently ignore cleanup errors
    })
  }
}

## Prepare all data for async Gemini call (synchronous orchestration)
geminiPrepareAll <- function(values, activeTab, input) {
  field <- geminiFieldName(activeTab)
  if (is.null(field)) {
    return(list(field = "MainInfoGemini", can_proceed = FALSE,
                error_msg = "Unsupported tab for Biblio AI."))
  }

  # Build description
  gemini_additional <- values$gemini_additional
  gemini_model_parameters <- values$gemini_model_parameters
  if (!is.null(gemini_additional) && gemini_additional != "") {
    desc <- paste0(
      values$collection_description,
      gemini_additional,
      gemini_model_parameters,
      collapse = ". "
    )
  } else {
    desc <- paste0(
      values$collection_description,
      gemini_model_parameters,
      collapse = ". "
    )
  }

  # Build prompt
  prompt <- biblioAiPrompts(values, activeTab)

  # Determine obj and type for each tab
  obj <- NULL
  type <- "text"

  switch(
    activeTab,
    "mainInfo" = {
      obj <- NULL
      type <- "text"
    },
    "lifeCycle" = {
      files <- DLC2Gemini(values$DLCplotYear, values$DLCplotCum)
      obj <- files
      type <- "multi"
    },
    "threeFieldPlot" = {
      obj <- values$TFP
      type <- "plotly"
    },
    "authorsProdOverTime" = {
      obj <- values$AUProdOverTime$graph
      type <- "ggplot2"
    },
    "correspAuthorCountry" = {
      obj <- values$MRCOplot
      type <- "ggplot2"
    },
    "mostLocalCitDoc" = {
      obj <- NULL
      type <- "text"
    },
    "trendTopic" = {
      obj <- values$trendTopics$graph
      type <- "ggplot2"
    },
    "ReferenceSpect" = {
      obj <- values$res$spectroscopy
      type <- "ggplot2"
    },
    "coOccurenceNetwork" = {
      obj <- values$COCnetwork$VIS
      type <- "vis"
    },
    "thematicMap" = {
      obj <- values$TMmap
      type <- "plotly"
    },
    "thematicEvolution" = {
      files <- TE2Gemini(values$nexus, values$TEplot)
      obj <- files
      type <- "multi"
    },
    "factorialAnalysis" = {
      obj <- values$plotCS
      type <- "plotly"
    },
    "coCitationNetwork" = {
      obj <- values$COCITnetwork$VIS
      type <- "vis"
    },
    "historiograph" = {
      obj <- values$histPlotVis$VIS
      type <- "vis"
    },
    "collabNetwork" = {
      obj <- values$COLnetwork$VIS
      type <- "vis"
    },
    "collabWorldMap" = {
      obj <- values$WMmap$g
      type <- "plotly"
    },
    "bradford" = {
      obj <- values$bradford$graph
      type <- "ggplot2"
    },
    "lotka" = {
      obj <- values$lotka$g
      type <- "ggplot2"
    }
  )

  # Prepare image (synchronous)
  prep <- geminiPrepareImage(
    obj = obj,
    type = type,
    prompt = prompt,
    key = values$geminiAPI,
    desc = desc,
    values = values
  )

  if (!is.null(prep$error_msg)) {
    return(list(
      field = field,
      can_proceed = FALSE,
      error_msg = prep$error_msg,
      prompt = NULL,
      image_paths = NULL
    ))
  }

  return(list(
    field = field,
    can_proceed = TRUE,
    error_msg = NULL,
    prompt = prep$prompt,
    image_paths = prep$image_paths
  ))
}

geminiSave <- function(values, activeTab) {
  switch(
    activeTab,
    "mainInfo" = {
      gemini <- values$MainInfoGemini
    },
    "lifeCycle" = {
      gemini <- values$DLCGemini
    },
    "threeFieldPlot" = {
      gemini <- values$TFPGemini
    },
    "authorsProdOverTime" = {
      gemini <- values$ApotGemini
    },
    "correspAuthorCountry" = {
      gemini <- values$MostRelCountriesGemini
    },
    "mostLocalCitDoc" = {
      gemini <- values$MostLocCitDocsGemini
    },
    "trendTopic" = {
      gemini <- values$trendTopicsGemini
    },
    "ReferenceSpect" = {
      gemini <- values$rpysGemini
    },
    "coOccurenceNetwork" = {
      gemini <- values$cocGemini
    },
    "thematicMap" = {
      gemini <- values$TMGemini
    },
    "thematicEvolution" = {
      gemini <- values$TEGemini
    },
    "factorialAnalysis" = {
      gemini <- values$CSGemini
    },
    "coCitationNetwork" = {
      gemini <- values$cocitGemini
    },
    "historiograph" = {
      gemini <- values$histGemini
    },
    "collabNetwork" = {
      gemini <- values$colGemini
    },
    "collabWorldMap" = {
      gemini <- values$WMGemini
    },
    "bradford" = {
      gemini <- values$BradfordGemini
    },
    "lotka" = {
      gemini <- values$LotkaGemini
    }
  )
  if (is.null(gemini)) {
    gemini <- "Click 'Ask Biblio AI' for help. "
  }
  return(gemini)
}

## Summarize document with Gemini AI ---
summaryAI <- function(values, i, model) {
  # Construct the prompt for Gemini AI
  prompt <- paste0(
    "Summarize the content of the following publication",
    " in no more than 250 words. ",
    "Title: ",
    values$M$TI[i],
    "Authors: ",
    values$M$AU[i],
    "Journal: ",
    values$M$SO[i],
    "Publication Year: ",
    values$M$PY[i],
    "Abstract: ",
    values$M$AB[i],
    "DOI: ",
    values$M$DI[i],
    "Try to access to article website using its DOI and extract more information about the article content. ",
    "If the article is open access, try to extract the full text and summarize it. ",
    "If the article is not open access, try to extract more information from the article website. ",
    "If you cannot access to the article website, just summarize the content using the title and the abstract. ",
    "Do not mention that you are an AI model. ",
    "Do not mention that you cannot access to the article website. ",
    "Do not mention that you do not have access to the full text. ",
    "Just provide the summary. "
  )

  # Gemini AI call
  res <- tryCatch(
    {
      gemini_ai(
        image = NULL,
        prompt = prompt,
        model = model,
        image_type = "png",
        retry_503 = 2,
        api_key = NULL
      )
    },
    error = function(e) {
      "Error generating automatic summary."
    }
  )

  return(res)
}


## Normalize abstract text
normalize_uppercase_text <- function(text) {
  # Check that input is a character string
  if (!is.character(text) || length(text) != 1) {
    stop("Input must be a character string")
  }

  # Convert everything to lowercase
  text_lower <- tolower(text)

  # Capitalize the first letter of the text
  text_lower <- paste0(
    toupper(substring(text_lower, 1, 1)),
    substring(text_lower, 2)
  )

  # Pattern to identify end of sentence followed by space and letter
  # Considers period, exclamation mark, question mark
  text_normalized <- gsub(
    "([.!?])\\s+([a-z])",
    "\\1 \\U\\2",
    text_lower,
    perl = TRUE
  )

  # Handle common abbreviations that should not be followed by uppercase
  # Examples: Dr., Prof., etc., vs., i.e., e.g.
  common_abbrev <- c(
    "dr\\.",
    "prof\\.",
    "etc\\.",
    "vs\\.",
    "i\\.e\\.",
    "e\\.g\\.",
    "mr\\.",
    "mrs\\.",
    "ms\\.",
    "jr\\.",
    "sr\\.",
    "phd\\."
  )

  for (abbrev in common_abbrev) {
    # Convert to lowercase the letters after common abbreviations
    pattern <- paste0("(", abbrev, ")\\s+([A-Z])")
    text_normalized <- gsub(
      pattern,
      "\\1 \\L\\2",
      text_normalized,
      perl = TRUE,
      ignore.case = TRUE
    )
  }

  # Capitalize after colons (optional - common in academic titles)
  text_normalized <- gsub(
    "(:)\\s+([a-z])",
    "\\1 \\U\\2",
    text_normalized,
    perl = TRUE
  )

  return(text_normalized)
}

## To title upper case metadata fields
to_title_case <- function(texts, exclude_articles = TRUE) {
  # --- Input Validation ---
  # Check that the input is a character vector. If not, stop and show an error.
  if (!is.character(texts)) {
    stop("Input must be a character vector.")
  }

  # --- Configuration ---
  # Define a list of "small words" (articles, prepositions, conjunctions) that
  # should remain in lowercase unless they are the first or last word in the title.
  small_words <- c(
    "a",
    "an",
    "and",
    "as",
    "at",
    "but",
    "by",
    "for",
    "if",
    "in",
    "of",
    "on",
    "or",
    "the",
    "to",
    "up",
    "via",
    "with",
    "from",
    "into",
    "over",
    "upon",
    "down",
    "off",
    "out",
    "per",
    "than",
    "through",
    "under",
    "within",
    "without",
    "about",
    "across",
    "after",
    "against",
    "along",
    "among",
    "around",
    "before",
    "behind",
    "below",
    "beneath",
    "beside",
    "between",
    "beyond",
    "during",
    "except",
    "inside",
    "near",
    "since",
    "toward",
    "towards",
    "until",
    "where",
    "while"
  )

  # --- Helper Function ---
  # A helper function to capitalize the first letter of a single word.
  # It also handles hyphenated words (e.g., "state-of-the-art" -> "State-of-the-Art").
  capitalize_first <- function(word) {
    # Check for hyphens
    if (grepl("-", word)) {
      # If a hyphen exists, split the word into parts
      parts <- strsplit(word, "-")[[1]]
      # Capitalize the first letter of each part
      capitalized_parts <- sapply(parts, function(part) {
        if (nchar(part) > 0) {
          paste0(toupper(substring(part, 1, 1)), substring(part, 2))
        } else {
          part # Return empty parts as they are
        }
      })
      # Re-join the parts with a hyphen
      return(paste(capitalized_parts, collapse = "-"))
    } else {
      # If no hyphen, just capitalize the first letter of the word
      paste0(toupper(substring(word, 1, 1)), substring(word, 2))
    }
  }

  # --- Main Processing Logic ---
  # Use sapply() to apply the title-casing logic to each element of the input vector 'texts'.
  # 'sapply' iterates over the vector and collects the results into a new vector.
  # USE.NAMES = FALSE ensures the output is an unnamed vector.
  result_vector <- sapply(
    texts,
    function(single_text) {
      # Handle edge cases: return NA or empty strings without modification.
      if (is.na(single_text) || nchar(single_text) == 0) {
        return(single_text)
      }

      # Convert the entire string to lowercase to ensure consistency.
      text_lower <- tolower(single_text)

      # Split the string into a vector of words based on whitespace.
      words <- strsplit(text_lower, "\\s+")[[1]]

      # Apply capitalization based on the 'exclude_articles' flag.
      if (exclude_articles) {
        # If TRUE, apply capitalization selectively.
        capitalized_words <- sapply(seq_along(words), function(i) {
          word <- words[i]
          # Always capitalize the first word, the last word, or any word NOT in the 'small_words' list.
          if (i == 1 || i == length(words) || !word %in% small_words) {
            capitalize_first(word)
          } else {
            word # Otherwise, keep the small word in lowercase.
          }
        })
      } else {
        # If FALSE, capitalize every word.
        capitalized_words <- sapply(words, capitalize_first)
      }

      # Recombine the processed words into a single string, separated by spaces.
      paste(capitalized_words, collapse = " ")
    },
    USE.NAMES = FALSE
  )

  # Return the final vector of title-cased strings.
  return(result_vector)
}


merge_df_to_string <- function(df) {
  # Check if the input has at least two columns
  if (ncol(df) < 2) {
    stop("The data frame must have at least two columns.")
  }

  # Ensure the input is a data frame
  df <- as.data.frame(df)

  # Convert each row into a "param: value" format
  row_strings <- apply(df[, 1:2], 1, function(row) {
    paste0(row[1], ": ", row[2])
  })

  # Concatenate all row strings using "; " as separator
  final_string <- paste(row_strings, collapse = "; ")

  return(final_string)
}

plot2pngGemini <- function(p, filename, zoom = 2, type = "vis") {
  #html_name <- tempfile(fileext = ".html")
  # remove file extensions from filename
  filename_html <- tools::file_path_sans_ext(filename)
  temp_html <- paste0(filename_html, ".html")
  switch(
    type,
    vis = {
      visSave(p, temp_html)
    },
    plotly = {
      htmlwidgets::saveWidget(p, file = temp_html, selfcontained = TRUE)
    }
  )

  biblioShot(temp_html, zoom = zoom, file = filename, delay = 2.5)
}

getWD <- function() {
  home_path <- Sys.getenv("HOME")
  temp_path <- file.path(home_path, "temp")

  if (!dir.exists(temp_path)) {
    dir.create(temp_path, recursive = TRUE)
  }

  return(temp_path)
}

## save all plots of the life cycle analysis
DLC2Gemini <- function(DLCplotYear, DLCplotCum) {
  plots <- c("annual", "cumulative")
  files <- unlist(lapply(plots, function(x) {
    paste0(getWD(), "/plotDLC_", x, ".png")
  }))

  plot2pngGemini(DLCplotYear, filename = files[1], type = "plotly")
  plot2pngGemini(DLCplotCum, filename = files[2], type = "plotly")

  return(files)
}

## save all plots of the thematic evolution analysis
TE2Gemini <- function(nexus, plotTE) {
  K <- length(nexus$TM)
  periods <- nexus$Nodes %>% select(group) %>% distinct() %>% pull()
  files <- unlist(lapply(periods, function(x) {
    file.path(getWD(), paste0(x, "_period.png"))
  }))
  files <- c(file.path(getWD(), "Evolution_Plot.png"), files)

  plot2pngGemini(plotTE, filename = files[1], type = "plotly")

  for (k in 2:(K + 1)) {
    suppressMessages(ggsave(
      filename = files[k],
      plot = nexus$TM[[k - 1]]$map +
        ggplot2::labs(
          title = paste0("Period: ", periods[k - 1]),
          dpi = 72,
          height = 7,
          width = 14,
          bg = "transparent"
        )
    ))
  }
  return(files)
}

copy_to_clipboard <- function(x) {
  # Check the operating system
  sys_info <- Sys.info()
  os_type <- tolower(sys_info["sysname"])

  # Convert the object to a string if it is not already
  if (!is.character(x)) {
    x <- capture.output(print(x))
  }

  # Copy to clipboard based on the operating system
  if (os_type == "windows") {
    writeClipboard(x)
  } else if (os_type == "darwin") {
    # macOS
    con <- pipe("pbcopy", "w")
    writeLines(x, con)
    close(con)
  } else if (os_type == "linux") {
    # Use xclip or xsel, if available
    if (nzchar(Sys.which("xclip"))) {
      con <- pipe("xclip -selection clipboard", "w")
      writeLines(x, con)
      close(con)
    } else if (nzchar(Sys.which("xsel"))) {
      con <- pipe("xsel --clipboard --input", "w")
      writeLines(x, con)
      close(con)
    } else {
      stop(
        "Neither 'xclip' nor 'xsel' are available. Please install one of them to use the clipboard on Linux."
      )
    }
  } else {
    stop("Unrecognized or unsupported operating system.")
  }
}

## New function to convert gemini output as HTML blocks
gemini_to_html <- function(text, font_size = "16px") {
  # Remove original leading/trailing whitespace
  text <- trimws(text)

  # Divide text into lines
  lines <- unlist(strsplit(text, "\n", fixed = TRUE))

  # Initialize HTML output with CSS styles including font size
  html_lines <- c(
    paste0(
      "<div style='font-family: Arial, sans-serif; line-height: 1.4; margin: 0 auto; padding: 15px; font-size: ",
      font_size,
      ";'>"
    )
  )

  in_list <- FALSE
  list_type <- ""
  last_ol_number <- 0

  close_list <- function() {
    if (in_list) {
      if (list_type == "ul") {
        html_lines <<- c(html_lines, "</ul>")
      } else {
        html_lines <<- c(html_lines, "</ol>")
      }
      in_list <<- FALSE
    }
  }

  for (i in seq_along(lines)) {
    line <- trimws(lines[i])

    # Skip empty lines
    if (line == "") next

    # Skip horizontal rules (--- or ***)
    if (grepl("^[-*_]{3,}\\s*$", line)) next

    # Markdown header management (lines starting with #, ##, ###, ####)
    if (stringr::str_detect(line, "^#{1,4}\\s+")) {
      close_list()

      header_level <- nchar(stringr::str_extract(line, "^#+"))
      header_text <- stringr::str_replace(line, "^#+\\s+", "")
      header_text <- stringr::str_replace(header_text, "\\s*#+\\s*$", "")
      header_text <- stringr::str_replace_all(header_text, "\\*\\*([^*]+)\\*\\*", "\\1")

      h_tag <- paste0("h", min(header_level + 1, 5))
      h_size <- switch(as.character(header_level),
        "1" = "font-size: 1.3em;",
        "2" = "font-size: 1.15em;",
        "3" = "font-size: 1.05em;",
        "font-size: 1em;"
      )
      html_lines <- c(
        html_lines,
        paste0(
          "<", h_tag, " style='color: #333; border-bottom: 1px solid #007acc; padding-bottom: 4px; margin-bottom: 6px; margin-top: 14px; ", h_size, "'>",
          header_text,
          "</", h_tag, ">"
        )
      )
      next
    }

    # Title management (lines enclosed in ** only)
    if (stringr::str_detect(line, "^\\*\\*[^*]+\\*\\*$")) {
      close_list()

      title_text <- stringr::str_replace_all(line, "^\\*\\*(.+)\\*\\*$", "\\1")
      title_text <- stringr::str_replace(title_text, "^\\d+\\.\\s*", "")
      html_lines <- c(
        html_lines,
        paste0(
          "<h3 style='color: #333; border-bottom: 1px solid #007acc; padding-bottom: 4px; margin-bottom: 6px; margin-top: 14px;'>",
          title_text,
          "</h3>"
        )
      )
      next
    }

    # Managing bulleted lists (starting with * or -)
    if (stringr::str_detect(line, "^\\s*[*\\-]\\s+")) {
      if (!in_list || list_type != "ul") {
        close_list()
        html_lines <- c(
          html_lines,
          "<ul style='margin-bottom: 6px; margin-top: 4px;'>"
        )
        in_list <- TRUE
        list_type <- "ul"
      }

      item_text <- stringr::str_replace(line, "^\\s*[*\\-]\\s+", "")
      item_text <- format_inline_text(item_text)

      html_lines <- c(
        html_lines,
        paste0("<li style='margin-bottom: 2px;'>", item_text, "</li>")
      )
      next
    }

    # Managing numbered lists (starting with a number followed by a period)
    if (stringr::str_detect(line, "^\\s*\\d+\\.\\s+")) {
      item_number <- as.integer(stringr::str_extract(line, "^\\s*\\d+"))

      if (!in_list || list_type != "ol") {
        close_list()
        start_attr <- if (item_number > 1) paste0(" start='", item_number, "'") else ""
        html_lines <- c(
          html_lines,
          paste0("<ol style='margin-bottom: 6px; margin-top: 4px;'", start_attr, ">")
        )
        in_list <- TRUE
        list_type <- "ol"
      }

      last_ol_number <- item_number
      item_text <- stringr::str_replace(line, "^\\s*\\d+\\.\\s+", "")
      item_text <- format_inline_text(item_text)
      html_lines <- c(
        html_lines,
        paste0("<li style='margin-bottom: 2px;'>", item_text, "</li>")
      )
      next
    }

    # Non-list, non-header line: close any open list
    close_list()

    # Normal paragraph management
    formatted_line <- format_inline_text(line)
    html_lines <- c(
      html_lines,
      paste0("<p style='margin-bottom: 5px; margin-top: 2px;'>", formatted_line, "</p>")
    )
  }

  # Close any lists that remain open
  close_list()

  # Close the container div
  html_lines <- c(html_lines, "</div>")

  # Merge all lines
  html_result <- paste(html_lines, collapse = "\n")

  return(html_result)
}

# Auxiliary function for formatting inline text
format_inline_text <- function(text) {
  # Bold management (**text**)
  text <- stringr::str_replace_all(
    text,
    "\\*\\*([^*]+)\\*\\*",
    "<strong>\\1</strong>"
  )

  # Italic management (*text*) - only if it is not already in bold
  text <- stringr::str_replace_all(
    text,
    "(?<!\\*)\\*([^*]+)\\*(?!\\*)",
    "<em>\\1</em>"
  )

  # Handling text in quotation marks as inline code ("text")
  text <- stringr::str_replace_all(
    text,
    '"([^"]+)"',
    '<code style="background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; font-family: \'Courier New\', monospace;">\\1</code>'
  )

  # Handling parentheses with percentages or numerical values
  text <- stringr::str_replace_all(
    text,
    "\\(([^)]*%[^)]*)\\)",
    "<span style='color: #007acc; font-weight: bold;'>(\\1)</span>"
  )

  return(text)
}


# Thematic Map top 3 documents for each cluster
doc2clust <- function(res, n = 3) {
  df <- res$doc2clust %>%
    drop_na(Assigned_cluster) %>%
    group_by(Assigned_cluster) %>%
    slice_max(pagerank, n = n) %>%
    mutate(cluster = paste(SR, ", pagerank ", round(pagerank, 3), sep = "")) %>%
    select(Assigned_cluster, cluster)
}

# APOT top 3 Documents per author
apot2Docs <- function(res, n = 3) {
  df <- res$dfPapersAU %>%
    group_by(Author) %>%
    slice_max(TCpY, n = n) %>%
    mutate(
      SR = paste(TI, SO, year, paste0("TCpY ", round(TCpY, 1)), sep = ", ")
    ) %>%
    select(Author, SR)
}

# Country collaboration table
country2collab <- function(df, n = 20) {
  df <- df %>%
    slice_max(Articles, n = n) %>%
    mutate(
      SR = paste(
        paste0("Article ", Articles, sep = ", "),
        paste0("SCP ", SCP, sep = ", "),
        paste0("MCP ", MCP, sep = ", "),
        paste0("MCP % ", round(`MCP %`, 1), sep = ", ")
      )
    ) %>%
    select(Country, SR)
}

#Local citation table
localCit2docs <- function(df, n = 20) {
  df <- df %>%
    slice_max(Local.Citations, n = n) %>%
    mutate(
      SR = paste(
        paste0("LC ", Local.Citations, sep = ", "),
        paste0("GC ", Global.Citations, sep = ", "),
        paste0("NLC ", round(Normalized.Local.Citations, 2), sep = ", "),
        paste0("NGC ", round(Normalized.Global.Citations, 2)),
        sep = ""
      )
    ) %>%
    select(Document, SR)
}

# Historiograph doc titles
hist2docs <- function(df, n = 20) {
  df %>%
    slice_max(order_by = LCS, n = n) %>%
    select(short_label, title_orig)
}

# Top 10 peaks in RPYS
rpysPeaks <- function(res, n = 10) {
  df_peaks <- res$rpysTable %>%
    arrange(Year) %>%
    mutate(
      is_peak = (diffMedian > lag(diffMedian)) & (diffMedian > lead(diffMedian))
    ) %>%
    dplyr::filter(is_peak) %>%
    arrange(desc(diffMedian)) %>%
    slice_head(n = n)

  df2 <- res$CR %>%
    group_by(Year) %>%
    slice_max(Freq, n = 3, with_ties = FALSE) %>%
    dplyr::filter(Year %in% df_peaks$Year)

  return(peaks = df2)
}
