#' Chat with a local Ollama model
#'
#' @description
#' To use `chat_ollama()` first download and install
#' [Ollama](https://ollama.com). Then install some models either from the
#' command line (e.g. with `ollama pull llama3.1`) or within R using
#' {[ollamar](https://hauselin.github.io/ollama-r/)} (e.g.
#' `ollamar::pull("llama3.1")`).
#'
#' Built on top of [chat_openai_compatible()].
#'
#' ## Known limitations
#'
#' * Tool calling is not supported with streaming (i.e. when `echo` is
#'   `"text"` or `"all"`)
#' * Models can only use 2048 input tokens, and there's no way
#'   to get them to use more, except by creating a custom model with a
#'   different default.
#' * Tool calling generally seems quite weak, at least with the models I have
#'   tried it with.
#'
#' @inheritParams chat_openai
#' @param model `r param_model(NULL, "ollama")`
#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead.
#' @param credentials Ollama doesn't require credentials for local usage and in most
#'   cases you do not need to provide `credentials`.
#'
#'   However, if you're accessing an Ollama instance hosted behind a reverse
#'   proxy or secured endpoint that enforces bearer‐token authentication, you
#'   can set the `OLLAMA_API_KEY` environment variable or provide a callback
#'   function to `credentials`.
#' @param params Common model parameters, usually created by [params()].
#' @inherit chat_openai return
#' @family chatbots
#' @export
#' @examples
#' \dontrun{
#' chat <- chat_ollama(model = "llama3.2")
#' chat$chat("Tell me three jokes about statisticians")
#' }
chat_ollama <- function(
  system_prompt = NULL,
  base_url = Sys.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
  model,
  params = NULL,
  api_args = list(),
  echo = NULL,
  api_key = NULL,
  credentials = NULL,
  api_headers = character()
) {
  # ollama doesn't require an API key for local usage, but one might be needed
  # if ollama is served behind a proxy (see #501)
  credentials <- ollama_credentials(credentials, api_key)

  if (!has_ollama(base_url, credentials)) {
    cli::cli_abort("Can't find locally running ollama.")
  }

  models <- models_ollama(base_url, credentials)$id

  if (missing(model)) {
    cli::cli_abort(c(
      "Must specify {.arg model}.",
      i = "Locally installed models: {.str {models}}."
    ))
  } else if (!model %in% models) {
    cli::cli_abort(
      c(
        "Model {.val {model}} is not installed locally.",
        i = "Run {.code ollama pull {model}} in your terminal or {.run ollamar::pull(\"{model}\")} in R to install the model.",
        i = "See locally installed models with {.run ellmer::models_ollama()}."
      )
    )
  }

  echo <- check_echo(echo)

  provider <- ProviderOllama(
    name = "Ollama",
    base_url = file.path(base_url, "v1"), ## the v1 portion of the path is added for openAI compatible API
    model = model,
    params = params %||% params(),
    extra_args = api_args,
    credentials = credentials,
    extra_headers = api_headers
  )

  Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
}

ProviderOllama <- new_class(
  "ProviderOllama",
  parent = ProviderOpenAICompatible,
  properties = list(
    model = prop_string()
  )
)

ollama_credentials <- function(credentials = NULL, api_key = NULL) {
  as_credentials(
    "chat_ollama",
    function() Sys.getenv("OLLAMA_API_KEY", ""),
    credentials = credentials,
    api_key = api_key
  )
}

method(chat_params, ProviderOllama) <- function(provider, params) {
  # https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion
  standardise_params(
    params,
    c(
      frequency_penalty = "frequency_penalty",
      presence_penalty = "presence_penalty",
      seed = "seed",
      stop = "stop_sequences",
      temperature = "temperature",
      top_p = "top_p",
      top_k = "top_k",
      max_tokens = "max_tokens"
    )
  )
}

chat_ollama_test <- function(..., model = "qwen3:4b", echo = "none") {
  # model: Note that tests require a model with tool capabilities

  skip_if_no_ollama()
  testthat::skip_if_not(
    model %in% models_ollama()$id,
    sprintf("Ollama: model '%s' is not installed", model)
  )

  chat_ollama(..., model = model, echo = echo)
}

skip_if_no_ollama <- function() {
  if (!has_ollama()) {
    testthat::skip("ollama not found")
  }
}

#' @export
#' @rdname chat_ollama
models_ollama <- function(
  base_url = "http://localhost:11434",
  credentials = NULL
) {
  credentials <- as_credentials(
    "models_ollama",
    function() Sys.getenv("OLLAMA_API_KEY", ""),
    credentials = credentials
  )

  req <- request(base_url)
  req <- ellmer_req_credentials(req, credentials(), "Authorization")
  req <- req_url_path_append(req, "api/tags")
  resp <- req_perform(req)
  json <- resp_body_json(resp)

  names <- map_chr(json$models, "[[", "name")
  names <- gsub(":latest$", "", names)

  modified_at <- as.POSIXct(map_chr(json$models, "[[", "modified_at"))
  size <- map_dbl(json$models, "[[", "size")

  df <- data.frame(
    id = names,
    created_at = modified_at,
    size = size,
    capabilities = ollama_model_capabilities(base_url, names, credentials)
  )
  df[order(-xtfrm(df$created_at)), ]
}

the$ollama_cache <- new_environment()

ollama_model_details <- function(
  base_url,
  model,
  credentials = ollama_credentials()
) {
  # https://github.com/ollama/ollama/blob/main/docs/api.md#show-model-information
  if (env_has(the$ollama_cache, model)) {
    return(the$ollama_cache[[model]])
  }

  req <- request(base_url)
  req <- ellmer_req_credentials(req, credentials(), "Authorization")
  req <- req_url_path_append(req, "api/show")
  req <- req_body_json(req, list(model = model, verbose = FALSE))

  resp <- req_perform(req)

  details <- resp_body_json(resp)

  # Cache model information (very unlikely to change during a session)
  the$ollama_cache[[model]] <- details
  details
}

ollama_model_capabilities <- function(
  base_url,
  models,
  credentials = ollama_credentials()
) {
  res <- map(models, function(m) {
    tryCatch(
      ollama_model_details(base_url, m, credentials),
      error = function(e) NULL
    )
  })
  map_chr(res, \(x) paste(x$capabilities, collapse = ","))
}


has_ollama <- function(
  base_url = "http://localhost:11434",
  credentials = ollama_credentials()
) {
  check_credentials(credentials)

  tryCatch(
    {
      req <- request(base_url)
      req <- ellmer_req_credentials(req, credentials(), "Authorization")
      req <- req_url_path_append(req, "api/tags")
      req_perform(req)
      TRUE
    },
    httr2_error = function(cnd) FALSE
  )
}

method(as_json, list(ProviderOllama, TypeObject)) <- function(
  provider,
  x,
  ...
) {
  if (x@additional_properties) {
    cli::cli_abort("{.arg .additional_properties} not supported for Ollama.")
  }

  # Unlike OpenAI, Ollama uses the `required` field to list required tool args
  required <- map_lgl(x@properties, function(prop) prop@required)

  compact(list(
    type = "object",
    description = x@description %||% "",
    properties = as_json(provider, x@properties, ...),
    required = as.list(names2(x@properties)[required]),
    additionalProperties = FALSE
  ))
}
