EngineRestGitHub <- R6::R6Class(
  classname = "EngineRestGitHub",
  inherit = EngineRest,
  public = list(

    get_files = function(file_paths, verbose = TRUE) {
      files_list <- list()
      for (filename in file_paths) {
        search_file_endpoint <- paste0(private$endpoints[["search"]], "filename:", filename)
        total_n <- self$response(
          endpoint = search_file_endpoint,
          verbose = verbose
        )[["total_count"]]
        if (length(total_n) > 0) {
          search_result <- private$search_response(
            search_endpoint = search_file_endpoint,
            total_n = total_n,
            verbose = verbose
          ) |>
            purrr::keep(~ .$path == filename)
          files_content <- private$get_files_content(search_result, filename)
          files_list <- append(files_list, files_content)
        }
      }
      return(files_list)
    },

    prepare_files_table = function(files_list) {
      files_table <- NULL
      if (!is.null(files_list)) {
        files_table <- purrr::map(files_list, function(file_data) {
          repo_fullname <- private$get_repo_fullname(file_data$url)
          org_repo <- stringr::str_split_1(repo_fullname, "/")
          data.frame(
            "repo_id" = NA_character_,
            "repo_name" = org_repo[2],
            "organization" = org_repo[1],
            "file_path" = file_data$path,
            "file_content" = file_data$content,
            "file_size" = file_data$size,
            "repo_url" = private$set_repo_url(file_data$url)
          )
        }) |>
          purrr::list_rbind()
      }
      return(files_table)
    },

    search_for_code = function(code,
                               org = NULL,
                               filename = NULL,
                               in_path = FALSE,
                               language = NULL,
                               verbose = TRUE) {
      query <- private$build_search_query(
        code = code, org = org, filename = filename,
        in_path = in_path, language = language
      )
      search_endpoint <- paste0(private$endpoints[["search"]], query)
      if (verbose) cli::cli_alert("Searching for code [{code}]...")
      total_n <- self$response(
        endpoint = search_endpoint,
        verbose = verbose
      )[["total_count"]]
      search_result <- if (length(total_n) > 0) {
        private$search_response(
          search_endpoint = search_endpoint,
          total_n = total_n,
          verbose = verbose
        )
      } else {
        list()
      }
      return(search_result)
    },

    search_repos_for_code = function(code,
                                     repos,
                                     filename = NULL,
                                     in_path = FALSE,
                                     language = NULL,
                                     verbose = TRUE) {
      if (verbose) cli::cli_alert("Searching for code [{code}]...")
      search_result <- purrr::map(repos, function(repo) {
        query <- private$build_search_query(
          code = code, repo = repo, filename = filename,
          in_path = in_path, language = language
        )
        search_endpoint <- paste0(private$endpoints[["search"]], query)
        total_n <- self$response(
          endpoint = search_endpoint,
          verbose = verbose
        )[["total_count"]]
        result <- if (length(total_n) > 0) {
          private$search_response(
            search_endpoint = search_endpoint,
            total_n = total_n,
            verbose = verbose
          )
        } else {
          list()
        }
        return(result)
      }) |>
        purrr::list_flatten()
      return(search_result)
    },

    get_repos_urls = function(type, org, repos, verbose = TRUE) {
      owner_type <- attr(org, "type") %||% "organization"
      if (owner_type == "user") {
        repo_endpoint <- paste0(private$endpoints[["users"]], url_decode(org), "/repos")
      } else {
        repo_endpoint <- paste0(private$endpoints[["organizations"]], url_decode(org), "/repos")
      }
      repos_response <- private$paginate_results(
        endpoint = repo_endpoint,
        verbose = verbose
      )
      if (!is.null(repos)) {
        repos_response <- repos_response |>
          purrr::keep(~ .$name %in% repos)
      }
      repos_urls <- repos_response |>
        purrr::map_vec(function(repository) {
          if (type == "api") {
            repository$url
          } else {
            repository$html_url
          }
        })
      return(repos_urls)
    },

    get_files_tree = function(org, repo, def_branch, pattern, depth, verbose) {
      endpoint <- paste0(
        private$endpoints[["repositories"]],
        org, "/", repo,
        "/git/trees/", def_branch,
        "?recursive=1"
      )
      response <- tryCatch(
        self$response(endpoint = endpoint, verbose = verbose),
        error = function(e) NULL
      )
      if (is.null(response) || inherits(response, "rest_api_error")) {
        return(NULL)
      }
      tree <- response$tree
      files <- purrr::keep(tree, ~ .$type == "blob") |>
        purrr::map_chr(~ .$path)
      if (!is.null(depth) && depth < Inf) {
        files <- purrr::keep(files, function(path) {
          stringr::str_count(path, "/") < depth
        })
      }
      if (!is.null(pattern)) {
        files <- purrr::keep(files, function(path) {
          any(stringr::str_detect(path, pattern))
        })
      }
      if (length(files) == 0) return(NULL)
      return(files)
    },

    get_repos_contributors = function(repos_table, verbose = TRUE, progress) {
      if (nrow(repos_table) > 0) {
        repo_iterator <- paste0(repos_table$organization, "/", repos_table$repo_name)
        user_name <- rlang::expr(.$login)
        repos_table$contributors <- gitstats_map_chr(repo_iterator, function(repos_id) {
          tryCatch({
            contributors_endpoint <- paste0(private$endpoints[["repositories"]], repos_id, "/contributors")
            contributors_vec <- private$get_contributors_from_repo(
              contributors_endpoint = contributors_endpoint,
              user_name = user_name,
              verbose = verbose
            )
            return(contributors_vec)
          },
          error = function(e) {
            NA
          })
        }, .progress = progress)
      }
      return(repos_table)
    }
  ),
  private = list(

    endpoints = list(
      search = NULL,
      organizations = NULL,
      repositories = NULL
    ),

    build_search_query = function(code, org = NULL, repo = NULL,
                                  filename = NULL, in_path = FALSE,
                                  language = NULL) {
      query <- utils::URLencode(code)
      if (!is.null(repo)) query <- paste0(query, '+repo:', repo)
      if (!is.null(org)) query <- paste0(query, '+user:', org)
      if (in_path) query <- paste0(query, '+in:path')
      if (!is.null(filename)) query <- paste0(query, '+in:file+filename:', filename)
      if (!is.null(language)) query <- paste0(query, '+language:', language)
      return(query)
    },

    set_endpoints = function() {
      private$endpoints[["search"]] <- paste0(
        self$rest_api_url,
        '/search/code?q='
      )
      private$endpoints[["organizations"]] <- paste0(
        self$rest_api_url,
        "/orgs/"
      )
      private$endpoints[["users"]] <- paste0(
        self$rest_api_url,
        "/users/"
      )
      private$endpoints[["repositories"]] <- paste0(
        self$rest_api_url,
        "/repos/"
      )
    },

    # @param byte_max According to GitHub documentation only files smaller than
    #   384 KB are searchable. See
    #   \link{https://docs.github.com/en/rest/search?apiVersion=2022-11-28#search-code}
    search_response = function(search_endpoint,
                               total_n,
                               byte_max = "384000",
                               verbose = TRUE) {
      if (total_n >= 0 & total_n < 1e3) {
        page_iterator <- 1:(total_n %/% 100)
        items_list <- purrr::map(page_iterator, function(page) {
          response <- self$response(
            endpoint = paste0(
              search_endpoint,
              "+size:0..",
              byte_max,
              "&page=",
              page,
              "&per_page=100"
            ),
            verbose = verbose
          )
          return(response[["items"]])
        }) |>
          purrr::list_flatten()
        return(items_list)
      } else if (total_n >= 1e3) {
        items_list <- list()
        index <- c(0, 50)
        spinner <- cli::make_spinner(
          which = "timeTravel",
          template = cli::col_grey(
            "GitHub search limit (1000 results) exceeded. Results will be divided. {spin}"
          )
        )
        while (index[2] < as.numeric(byte_max)) {
          size_formula <- paste0("+size:", as.character(index[1]), "..", as.character(index[2]))
          spinner$spin()
          n_count <- tryCatch({
            self$response(
              endpoint = paste0(search_endpoint, size_formula),
              verbose = verbose
            )[["total_count"]]
          }, error = function(e) {
            NULL
          })
          if (is.null(n_count)) {
            NULL
          } else if ((n_count - 1) %/% 100 > 0) {
            total_pages <- 1:(n_count %/% 100) + 1
            items_part_list <- purrr::map(total_pages, function(page) {
              self$response(
                endpoint = paste0(search_endpoint,
                                  size_formula,
                                  "&page=",
                                  page,
                                  "&per_page=100"),
                verbose = verbose
              )[["items"]]
            }) |>
              purrr::list_flatten()
            items_list <- append(items_list, items_part_list)
          } else if ((n_count - 1) %/% 100 == 0) {
            response <- self$response(
              endpoint = paste0(search_endpoint,
                                size_formula,
                                "&page=1&per_page=100"),
              verbose = verbose
            )
            items_list <- append(items_list, response[["items"]])
          }
          index[1] <- index[2]
          if (index[2] < 1e3) {
            index[2] <- index[2] + 50
          }
          if (index[2] >= 1e3 && index[2] < 1e4) {
            index[2] <- index[2] + 100
          }
          if (index[2] >= 1e4 && index[2] < 1e5) {
            index[2] <- index[2] + 1000
          }
          if (index[2] >= 1e5 && index[2] < 1e6) {
            index[2] <- index[2] + 10000
          }
        }
        spinner$finish()
        return(items_list)
      }
    },

    get_files_content = function(search_result, filename) {
      purrr::map(search_result, ~ self$response(.$url),
                 .progress = glue::glue("Adding file [{filename}] info...")) |>
        unique()
    },

    get_repo_fullname = function(file_url) {
      stringr::str_remove_all(file_url,
                              paste0(private$endpoints$repositories, "/")) |>
        stringr::str_replace_all("/contents.*", "")
    }
  )
)
