#' Retrieve Financial Data from the Financial Modeling Prep (FMP) API
#'
#' This function fetches financial data from the FMP API, including
#' balance sheet statements, income statements, cash flow statements,
#' historical market data, stock lists, and company profiles.
#'
#' @param resource A string indicating the API resource to query. Examples
#'  include `"balance-sheet-statement"`, `"income-statement"`,
#'  `"cash-flow-statement"`, `"historical-market-capitalization"`,
#'  `"profile"`, and `"stock/list"`.
#' @param symbol A string specifying the stock ticker symbol (optional).
#' @param params List of additional arguments to customize the query (optional).
#' @param api_version A string specifying the version of the FMP API to use.
#'  Defaults to `"v3"`.
#' @param snake_case A boolean indicating whether column names are converted
#'  to snake_case. Defaults to `TRUE`.
#'
#' @return A data frame containing the processed financial data.
#'
#' @export
#'
#' @examples
#' \dontrun{
#' # Get available balance sheet statements
#' fmp_get(
#'   resource = "balance-sheet-statement",
#'   symbol = "AAPL"
#' )
#'
#' # Get last income statements
#' fmp_get(
#'   resource = "income-statement",
#'   symbol = "AAPL",
#'   params = list(limit = 1)
#' )
#'
#' # Get annual cash flow statements
#' fmp_get(
#'   resource = "cash-flow-statement",
#'   symbol = "AAPL",
#'   params = list(period = "annual")
#' )
#'
#' # Get historical market capitalization
#' fmp_get(
#'   resource = "historical-market-capitalization",
#'   symbol = "UNH",
#'   params = list(from = "2023-12-01", to = "2023-12-31")
#' )
#'
#' # Get stock list
#' fmp_get(
#'   resource = "stock/list"
#' )
#'
#' # Get company profile
#' fmp_get(
#'   resource = "profile", symbol = "AAPL"
#' )
#'
#' # Search for stock information
#' fmp_get(
#'   resource = "search", params = list(query = "AAP")
#' )
#'
#' # Get data with original column names
#' fmp_get(
#'   resource = "profile", symbol = "AAPL", snake_case = FALSE
#' )
#' }
#'
fmp_get <- function(
  resource,
  symbol = NULL,
  params = list(),
  api_version = "stable",
  snake_case = TRUE
) {
  resource_processed <- build_resource(resource, symbol, api_version)

  if (api_version == "stable" && !is.null(symbol)) {
    params$symbol <- symbol
  }

  if (!is.null(params$limit)) {
    validate_limit(params$limit)
  }

  if (!is.null(params$period)) {
    validate_period(params$period)
  }

  data_raw <- perform_request(
    resource_processed,
    params,
    api_version = api_version
  )

  data_processed <- data_raw |>
    bind_rows() |>
    convert_column_types()

  if (snake_case) {
    data_processed <- data_processed |>
      convert_column_names()
  }

  data_processed
}

#' @keywords internal
#' @noRd
build_resource <- function(resource, symbol, api_version) {
  if (!is.null(symbol)) {
    validate_symbol(symbol)
    if (api_version == "stable") {
      resource_processed <- resource
    } else {
      resource_processed <- paste0(resource, "/", symbol)
    }
  } else {
    resource_processed <- resource
  }
  resource_processed
}

#' Perform a request to the Financial Modeling Prep API
#'
#' This function sends a request to the Financial Modeling Prep (FMP) API based
#' on the specified resource and additional query parameters. It constructs the
#' request URL using the base API URL and the specified version, and it
#' automatically includes the API key from the environment.
#'
#' @param resource The specific API resource to be accessed, such as a stock
#'  symbol or financial endpoint.
#' @param params Additional query parameters to be included in the API request.
#' @param base_url The base URL for the FMP API. Defaults to
#' "https://financialmodelingprep.com/api/".
#' @param api_version The version of the FMP API to use. Defaults to "stable".
#'
#' @return A parsed JSON response from the FMP API.
#'
#' @keywords internal
#' @noRd
perform_request <- function(
  resource,
  params,
  base_url = "https://financialmodelingprep.com/",
  api_version = "stable"
) {
  base_url <- build_base_url(base_url, api_version)

  req <- create_request(base_url, api_version, resource, params)

  resp <- req |>
    req_perform()

  if (resp$status_code != 200) {
    cli::cli_abort(
      resp_body_json(resp)$`Error Message`
    )
  } else {
    body <- resp_body_json(resp)

    validate_body(body)

    body
  }
}

#' @keywords internal
#' @noRd
build_base_url <- function(base_url, api_version) {
  if (api_version %in% c("v1", "v2", "v3")) {
    base_url <- paste0(base_url, "api/")
  }
  base_url
}

#' @keywords internal
#' @noRd
create_request <- function(base_url, api_version, resource, params) {
  validate_api_key()

  request(base_url) |>
    req_url_path_append(api_version) |>
    req_url_path_append(resource) |>
    req_url_query(
      apikey = Sys.getenv("FMP_API_KEY"),
      !!!params,
      .multi = "comma"
    ) |>
    req_user_agent(
      "fmpapi R package (https://github.com/tidy-finance/r-fmpapi)"
    ) |>
    req_error(is_error = \(resp) FALSE)
}

#' @keywords internal
#' @noRd
validate_api_key <- function() {
  # nocov start
  if (Sys.getenv("FMP_API_KEY") == "") {
    cli::cli_abort(
      "Please set an API key using `fmp_set_api_key()`"
    )
  } else {
    invisible(TRUE)
  }
  # nocov end
}

#' @keywords internal
#' @noRd
validate_symbol <- function(symbol) {
  if (length(symbol) != 1) {
    cli::cli_abort(
      "Please provide a single {.arg symbol}."
    )
  }
}

#' @keywords internal
#' @noRd
validate_period <- function(period) {
  if (!period %in% c("annual", "quarter")) {
    cli::cli_abort(
      "{.arg period} must be either 'annual' or 'quarter'."
    )
  }
}

#' @keywords internal
#' @noRd
validate_limit <- function(limit) {
  if (!is.numeric(limit) || limit %% 1L != 0 || limit < 1L) {
    cli::cli_abort("{.arg limit} must be an integer larger than 0.")
  }
}

#' @keywords internal
#' @noRd
validate_body <- function(body) {
  if (length(body) == 0) {
    cli::cli_warn(
      "Response body is empty. Check your resource and parameter specification."
    )
  }
}

#' Convert Column Names to snake_case
#'
#' @param df A data frame whose column names are to be converted.
#'
#' @return The data frame with its column names converted to snake_case.
#'
#' @keywords internal
#' @noRd
convert_column_names <- function(df) {
  new_names <- gsub("([a-z])([A-Z])", "\\1_\\2", names(df))
  new_names <- tolower(new_names)
  colnames(df) <- new_names
  df
}

#' Convert Column Types in a Data Frame
#'
#' @param df A data frame containing columns to be processed.
#'
#' @return A data frame with updated column types.
#'
#' @keywords internal
#' @noRd
convert_column_types <- function(df) {
  df |>
    mutate(
      across(contains("calendarYear"), as.integer),
      across(c(contains("Date"), contains("date")), function(x) {
        x[x == ""] <- NA_character_
        posix_converted <- as.POSIXct(x, tz = "UTC", optional = TRUE)
        if (all(is.na(posix_converted))) {
          return(as.Date(rep(NA, length(x))))
        }
        has_time <- any(
          format(posix_converted, "%H:%M:%S") != "00:00:00",
          na.rm = TRUE
        )
        if (!has_time) {
          return(as.Date(x))
        }
        posix_converted
      })
    )
}
