#' Get PHS Open Data using SQL
#'
#' @description
#' Downloads data from the NHS Open Data platform using a SQL query. Similar to
#' [get_resource()], but allows more flexible server-side querying. This
#' function has a lower maximum row number (32,000 vs 99,999) for returned
#' results.
#'
#' @param sql A single `SELECT` query (character).
#'
#' @details
#' Only 32,000 rows can be returned from a single SQL query.
#'
#' @section SQL syntax:
#' The resource ID must be double-quoted, e.g.
#' `SELECT * FROM "58527343-a930-4058-bf9e-3c6e5cb04010"`, as must column names,
#' e.g. `"Year"`. Strings require single quotes, e.g. `'value'`. This syntax is
#' needed because the CKAN DataStore uses
#' [PostgreSQL](https://www.postgresql.org/docs/9.2/index.html).
#'
#' @section R raw strings:
#' Enclosing the query in an R raw string avoids the need to escape embedded
#' quotes, e.g. `\"TotalCancelled\"`. Square brackets are the recommended
#' delimiter, i.e. `r"[...]"`, because `)"` within a query, e.g.
#' `SUM("TotalCancelled")`, would prematurely close the string. Another option
#' is `r"{...}"`, in the rare case that the query contains `]"`.
#'
#' @return
#' A [tibble][tibble::tibble-package] with the query results. Only 32,000 rows
#' can be returned from a single SQL query.
#'
#' @seealso
#' [get_resource()] for downloading a resource without using a SQL query.
#'
#' @export
#'
#' @examplesIf isTRUE(length(curl::nslookup("www.opendata.nhs.scot", error = FALSE)) > 0L)
#' # Basic query
#' cancelled_ops <- get_resource_sql(r"[
#' SELECT
#'     "TotalCancelled",
#'     "TotalOperations",
#'     "Hospital",
#'     "Month"
#' FROM
#'     "bcc860a4-49f4-4232-a76b-f559cf6eb885"
#' WHERE
#'     "Hospital" = 'D102H'
#' ]")
#'
#' # Joining two resources
#' hb_pop <- get_resource_sql(r"[
#' SELECT
#'     pops."Year",
#'     pops."HB",
#'     lookup."HBName",
#'     pops."AllAges"
#' FROM
#'     "27a72cc8-d6d8-430c-8b4f-3109a9ceadb1" AS pops
#'     JOIN "652ff726-e676-4a20-abda-435b98dd7bdc" AS lookup ON pops."HB" = lookup."HB"
#' WHERE
#'     pops."Sex" = 'All'
#'     AND pops."Year" > 2006
#' ]")
get_resource_sql <- function(sql) {
  if (length(sql) != 1L) {
    cli::cli_abort(c(
      x = "SQL validation error.",
      i = "{.var sql} must be length 1 not {length(sql)}."
    ))
  }

  if (!inherits(sql, "character")) {
    cli::cli_abort(c(
      x = "SQL validation error.",
      i = "{.var sql} must be of class {.cls character} not {.cls {class(sql)}}."
    ))
  }

  # check query is a SELECT statement
  if (!grepl("^\\s*?SELECT", sql)) {
    cli::cli_abort(c(
      x = "SQL validation error.",
      i = "{.var sql} must start with {.val SELECT}"
    ))
  }

  # Add the SQL statement to the query
  query <- list(sql = sql)

  # attempt get request
  content <- phs_GET("datastore_search_sql", query)

  if (!is.null(content[["result"]][["records_truncated"]])) {
    cli::cli_warn(
      "The data was truncated because your query matched more than the
      maximum number of rows."
    )
  }

  # extract the records (rows) from content
  query_data <- purrr::map(
    content$result$records,
    ~ {
      # replace NULL with "" so tibble works
      is_null <- purrr::map_lgl(.x, is.null)
      .x[is_null] <- ""

      tibble::as_tibble(.x)
    }
  ) %>%
    purrr::list_rbind()

  # If the query returned no rows, exit now.
  if (nrow(query_data) == 0L) {
    return(query_data)
  }

  # get correct order of columns
  col_order <- purrr::map_chr(
    content$result$fields,
    ~ .x$id
  )
  col_order <- col_order[!col_order %in% c("_id", "_full_text")]

  # select and reorder columns to reflect
  cleaner <- dplyr::select(query_data, dplyr::all_of(col_order))

  # warn if limit may have been surpassed
  if (nrow(cleaner) == 32000L) {
    cli::cli_warn(c(
      "Row number limit",
      i = "SQL queries are limitted to returning 32,000 results.
      This may have affected the results of your query."
    ))
  }

  return(cleaner)
}
