#' Detect if running in a hosted session
#'
#' Determines whether the current R session is running in a hosted environment
#' such as Google Colab, VS Code, Kubernetes, or RStudio Server (non-localhost).
#'
#' @return A logical value: `TRUE` if running in a hosted session (Google Colab,
#'   VS Code, Kubernetes, or remote RStudio Server), `FALSE` otherwise.
#'
#' @details
#' This function checks for (in order):
#' * Option override: if `azr.hosted` option is set, returns `isTRUE()` of its value
#'
#' * Google Colab: presence of the `COLAB_RELEASE_TAG` environment variable
#' * VS Code: presence of the `VSCODE_INJECTION` or `VSCODE_PROXY_URI` environment variable
#' * Kubernetes: presence of the `KUBERNETES_SERVICE_HOST` environment variable
#' * RStudio Server: `RSTUDIO_PROGRAM_MODE` is "server" and
#'   `RSTUDIO_HTTP_REFERER` does not contain "localhost"
#'
#' @export
#' @examples
#' if (is_hosted_session()) {
#'   message("Running in a hosted environment")
#' }
is_hosted_session <- function() {
  # Check for user override via option
  opt <- getOption("azr.hosted")
  if (!is.null(opt)) {
    return(isTRUE(opt))
  }

  # Check for Google Colab
  if (nzchar(Sys.getenv("COLAB_RELEASE_TAG"))) {
    return(TRUE)
  }

  # Check for VS Code
  if (nzchar(Sys.getenv("VSCODE_INJECTION"))) {
    return(TRUE)
  }

  if (nzchar(Sys.getenv("VSCODE_PROXY_URI"))) {
    return(TRUE)
  }

  # Check for Kubernetes
  if (nzchar(Sys.getenv("KUBERNETES_SERVICE_HOST"))) {
    return(TRUE)
  }

  # Check for RStudio Server (non-localhost)
  Sys.getenv("RSTUDIO_PROGRAM_MODE") == "server" &&
    !grepl("localhost", Sys.getenv("RSTUDIO_HTTP_REFERER"), fixed = TRUE)
}


bullets <- function(x) {
  as_simple <- function(x) {
    if (is.atomic(x) && length(x) == 1) {
      if (is.character(x)) {
        paste0("\"", x, "\"")
      } else {
        format(x)
      }
    } else {
      if (inherits(x, "redacted")) {
        format(x)
      } else {
        paste0("<", class(x)[[1L]], ">")
      }
    }
  }

  vals <- vapply(x, as_simple, character(1))
  names <- format(names(x))
  names <- gsub(" ", "\u00a0", names, fixed = TRUE) # non-breaking space

  for (i in seq_along(x)) {
    cli::cli_li("{.field {names[[i]]}}: {vals[[i]]}")
  }
  invisible(NULL)
}


list_redact <- function(x, names, case_sensitive = TRUE) {
  if (case_sensitive) {
    i <- match(names, names(x))
  } else {
    i <- match(tolower(names), tolower(names(x)))
  }
  i <- i[!is.na(i)]
  i <- setdiff(i, which(is_empty_vec(x)))
  x[i] <- list(redacted())
  x
}


redacted <- function() {
  structure(list(), class = "redacted")
}


#' @exportS3Method format redacted
format.redacted <- function(x, ...) {
  cli::col_grey("<REDACTED>")
}


#' @exportS3Method print redacted
print.redacted <- function(x, ...) {
  cat(format(x, ...), "\n", sep = "")
  invisible(x)
}

is_port_available <- function(port, host = "127.0.0.1") {
  # Try to connect to the port (if something is listening, connection succeeds)
  suppressWarnings(
    tryCatch(
      {
        con <- socketConnection(
          host = host,
          port = port,
          server = FALSE,
          blocking = TRUE,
          open = "r+",
          timeout = 1
        )
        close(con)
        # If connection succeeds, port is IN USE
        FALSE
      },
      error = function(e) {
        # If connection fails, port is AVAILABLE
        TRUE
      }
    )
  )
}

token_has_expired <- function(token, delay = 5) {
  if (!inherits(token, "httr2_token")) {
    cli::cli_abort(
      "{.arg token} must be an {.cls httr2_token} object, not {.cls {class(token)[[1L]]}}.",
      call = rlang::caller_env()
    )
  }
  if (is.null(token$expires_at)) {
    FALSE
  } else {
    (as.integer(Sys.time()) + delay) > token$expires_at
  }
}


random_port <- function(
  min = 10000L,
  max = 49151L,
  host = "127.0.0.1",
  n = 20
) {
  min <- max(1L, min)
  max <- min(max, 65535L)
  try_ports <- sample(x = seq.int(min, max), n)
  for (port in try_ports) {
    if (is_port_available(port, host)) {
      return(port)
    }
  }
  cli::cli_abort("Cannot find an available port.")
}
