#' Fetch a metadata database
#'
#' Fetch a SQLite database containing metadata from the gypsum backend
#' (see \url{https://github.com/ArtifactDB/bioconductor-metadata-index}).
#' Each database is generated by aggregating metadata across multiple assets and/or projects,
#' and can be used to perform searches for interesting objects.
#'
#' @param name String containing the name of the database.
#' This can be the name of any SQLite file in \url{https://github.com/ArtifactDB/bioconductor-metadata-index/releases/tag/latest}.
#' @param cache String containing the cache directory.
#' If \code{NULL}, no caching is performed.
#' @param overwrite Logical scalar indicating whether to overwrite an existing file in \code{cache}, if one is present.
#'
#' @return String containing a path to the downloaded database.
#'
#' @author Aaron Lun
#'
#' @details
#' This function will automatically check for updates to the SQLite files and will download new versions accordingly.
#' New checks are performed when one hour or more has elapsed since the last check.
#' If the check fails, a warning is raised and the function returns the currently cached file.
#'
#' @seealso
#' \code{\link{fetchMetadataSchema}}, to get the JSON schema used to define the database tables.
#' 
#' @examples
#' fetchMetadataDatabase()
#' @export
#' @importFrom filelock lock unlock
fetchMetadataDatabase <- function(name="bioconductor.sqlite3", cache=cacheDirectory(), overwrite=FALSE) {
    base.url <- "https://github.com/ArtifactDB/bioconductor-metadata-index/releases/download/latest/"

    if (is.null(cache)) {
        cache.path <- tempfile(fileext=".sqlite3")
    } else {
        cache.dir <- file.path(cache, "databases")
        cache.path <- file.path(cache.dir, name)
        dir.create(dirname(cache.path), showWarnings=FALSE, recursive=TRUE)

        if (file.exists(cache.path) && !overwrite) {
            # Seeing if we can acquire the lock, just to make sure that
            # no other process is downloading a new file at the same time.
            old_lastmod_raw <- (function() {
                flck <- lock(paste0(cache.path, ".LOCK"), exclusive=FALSE)
                on.exit(unlock(flck), add=TRUE, after=FALSE)
                readLines(paste0(cache.path, ".modified"), warn=FALSE)
            })()

            # Only returning the cached file if there haven't been any changes.
            old_lastmod <- as.double(old_lastmod_raw)
            new_lastmod <- get_last_modified_date(base.url)
            if (!is.null(new_lastmod) && old_lastmod == new_lastmod) {
                return(cache.path)
            }
        }
    }

    flck <- lock(paste0(cache.path, ".LOCK"))
    on.exit(unlock(flck), add=TRUE, after=FALSE)
    mod.path <- paste0(cache.path, ".modified")
    download_and_rename_file(paste0(base.url, "modified"), mod.path)
    download_and_rename_file(paste0(base.url, name), cache.path)

    last_check$req_time <- get_current_unix_time()
    last_check$mod_time <- as.double(readLines(mod.path, warn=FALSE))

    cache.path
}

last_check <- new.env()
last_check$req_time <- NULL
last_check$mod_time <- NULL

get_current_unix_time <- function() {
    as.double(Sys.time()) * 1000 # get to milliseconds
}

get_last_modified_date <- function(base.url) {
    # Only make a new request if an hour has elapsed; and even
    # then, don't sweat it if it fails, we'll just return NULL.
    curtime <- get_current_unix_time()
    if (!is.null(last_check$req_time) && last_check$req_time + 60 * 60 * 1000 >= curtime) {
        return(last_check$mod_time)
    }

    mod_time <- tryCatch({
        url <- paste0(base.url, "modified")
        req <- request(url)
        resp <- req_perform(req)
        as.double(resp_body_string(resp))
    }, error=function(e) {
        warning("failed to check the last modified timestamp; ", as.character(e))
        NULL
    })

    if (!is.null(mod_time)) {
        last_check$req_time <- curtime
        last_check$mod_time <- mod_time
    }
    mod_time
}
