GitHostGitHub <- R6::R6Class(
  classname = "GitHostGitHub",
  inherit = GitHost,
  public = list(
    initialize = function(orgs = NA,
                          repos = NA,
                          token = NA,
                          host = NA,
                          verbose = NA,
                          .error = TRUE) {
      super$initialize(orgs = orgs,
                       repos = repos,
                       token = token,
                       host = host,
                       verbose = verbose,
                       .error = .error)
      if (verbose) {
        cli::cli_alert_success("Set connection to GitHub.")
      }
    }
  ),
  private = list(

    host_name = "GitHub",
    api_version = 3,
    token_name = "GITHUB_PAT",
    min_access_scopes = c("public_repo", "read:org", "read:user"),

    access_scopes = list(
      org = c("read:org", "admin:org"),
      repo = c("public_repo", "repo"),
      user = c("read:user", "user")
    ),

    engine_methods = list(
      "graphql" = list(
        "repos",
        "commits",
        "release_logs"
      ),
      "rest" = list(
        "code",
        "contributors"
      )
    ),

    set_api_url = function(host) {
      if (is.null(host) ||
            host == "https://github.com" ||
            host == "http://github.com" ||
            host == "github.com") {
        private$api_url <- "https://api.github.com"
      } else {
        private$set_custom_api_url(host)
      }
    },

    set_web_url = function(host) {
      if (is.null(host)) {
        private$web_url <- "https://github.com"
      } else {
        private$set_custom_web_url(host)
      }
    },

    check_if_public = function(host) {
      private$is_public <- is.null(host) || grepl("github.com", host)
    },

    set_test_endpoint = function() {
      private$test_endpoint <- private$api_url
    },

    set_tokens_endpoint = function() {
      private$endpoints$tokens <- NULL
    },

    set_orgs_endpoint = function() {
      private$endpoints$orgs <- glue::glue("{private$api_url}/orgs")
      private$endpoints$users <- glue::glue("{private$api_url}/users")
    },

    set_repositories_endpoint = function() {
      private$endpoints$repositories <- glue::glue("{private$api_url}/repos")
    },

    setup_engines = function() {
      private$engines$rest <- EngineRestGitHub$new(
        rest_api_url = private$api_url,
        token = private$token,
        scan_all = private$scan_all
      )
      private$engines$graphql <- EngineGraphQLGitHub$new(
        gql_api_url = private$graphql_api_url,
        token = private$token,
        scan_all = private$scan_all
      )
    },

    check_token_scopes = function(response, token = NULL) {
      private$token_scopes <- response$headers$`x-oauth-scopes` |>
        stringr::str_split(", ") |>
        unlist()
      org_scopes <- any(private$access_scopes$org %in% private$token_scopes)
      repo_scopes <- any(private$access_scopes$repo %in% private$token_scopes)
      user_scopes <- any(private$access_scopes$user %in% private$token_scopes)
      all(c(org_scopes, repo_scopes, user_scopes))
    },

    add_repo_api_url = function(repos_table) {
      if (!is.null(repos_table) && nrow(repos_table) > 0) {
        repos_table <- dplyr::mutate(
          repos_table,
          api_url = paste0(private$endpoints$repositories, "/", organization, "/", repo_name),
        )
      }
      return(repos_table)
    },

    get_orgs_from_host = function(output, verbose) {
      if (private$is_public) {
        cli::cli_abort("This feature is not applicable for public hosts.",
                       call = NULL)
      }
      graphql_engine <- private$engines$graphql
      orgs <- graphql_engine$get_orgs(
        output = output,
        verbose = verbose
      )
      if (output == "full_table") {
        orgs <- orgs |>
          graphql_engine$prepare_orgs_table()
        private$orgs <- dplyr::pull(orgs, path)
      } else {
        private$orgs <- orgs
      }
      return(orgs)
    },

    get_repos_ids = function(search_response) {
      purrr::map_vec(search_response, ~.$repository$node_id) |> unique()
    },

    get_repo_url_from_response = function(search_response,
                                          repos_fullnames = NULL,
                                          type,
                                          verbose = TRUE,
                                          progress = TRUE) {
      if (!is.null(repos_fullnames)) {
        search_response <- search_response |>
          purrr::keep(~ paste0(.$organization$login, "/", .$repo_path) %in% repos_fullnames)
      }
      purrr::map_vec(search_response, function(project) {
        if (type == "api") {
          paste0(private$api_url, "/repos/", project$organization$login, "/", project$repo_name)
        } else {
          project$repo_url
        }
      })
    },

    get_commits_from_orgs = function(since,
                                     until,
                                     verbose,
                                     progress) {
      if ("org" %in% private$searching_scope) {
        graphql_engine <- private$engines$graphql
        commits_table <- gitstats_map(private$orgs, function(org) {
          commits_table_org <- NULL
          repos_data <- private$get_repos_data(
            org = org,
            verbose = verbose
          )
          if (!private$scan_all && verbose) {
            show_message(
              host = private$host_name,
              engine = "graphql",
              scope = org,
              information = paste0("Pulling commits ", cli_icons$commit)
            )
          }
          commits_table_org <- graphql_engine$get_commits_from_repos(
            org = org,
            repos_names = repos_data[["paths"]],
            since = since,
            until = until
          ) |>
            graphql_engine$prepare_commits_table(
              org = org
            )
          return(commits_table_org)
        }, .progress = set_progress_bar(progress, private)) |>
          purrr::list_rbind()
        return(commits_table)
      }
    },

    get_commits_from_repos = function(since,
                                      until,
                                      verbose,
                                      progress) {
      if ("repo" %in% private$searching_scope) {
        graphql_engine <- private$engines$graphql
        orgs <- graphql_engine$set_owner_type(
          owners = names(private$orgs_repos),
          verbose = verbose
        )
        commits_table <- gitstats_map(orgs, function(org) {
          commits_table_org <- NULL
          if (!private$scan_all && verbose) {
            show_message(
              host = private$host_name,
              engine = "graphql",
              scope = set_repo_scope(org, private),
              information = paste0("Pulling commits ", cli_icons$commit)
            )
          }
          commits_table_org <- graphql_engine$get_commits_from_repos(
            org = org,
            repos_names = private$orgs_repos[[org]],
            since = since,
            until = until
          ) |>
            graphql_engine$prepare_commits_table(
              org = org
            )
          return(commits_table_org)
        }, .progress = set_progress_bar(progress, private)) |>
          purrr::list_rbind()
        return(commits_table)
      }
    },

    get_files_content_from_repos = function(file_path,
                                            verbose = TRUE,
                                            progress = TRUE) {
      if ("repo" %in% private$searching_scope) {
        graphql_engine <- private$engines$graphql
        orgs <- graphql_engine$set_owner_type(
          owners = names(private$orgs_repos),
          verbose = verbose
        )
        files_table <- gitstats_map(orgs, function(org) {
          owner_type <- attr(org, "type") %||% "organization"
          repos_data <- private$get_repos_data(
            org = org,
            repos = private$orgs_repos[[org]],
            verbose = verbose
          )
          if (verbose) {
            show_message(
              host = private$host_name,
              engine = "graphql",
              scope = set_repo_scope(org, private),
              information = glue::glue("Pulling files {cli_icons$file} content: [{paste0(file_path, collapse = ', ')}]")
            )
          }
          graphql_engine$get_files_from_org(
            org = org,
            owner_type = owner_type,
            repos_data = repos_data,
            file_paths = file_path,
            verbose = verbose
          ) |>
            graphql_engine$prepare_files_table(
              org = org
            )
        }, .progress = set_progress_bar(progress, private)) |>
          purrr::list_rbind() |>
          private$add_repo_api_url()
        return(files_table)
      }
    },

    get_files_content_from_files_structure = function(files_structure,
                                                      verbose,
                                                      progress) {
      if (length(files_structure) > 0) {
        graphql_engine <- private$engines$graphql
        orgs <- names(files_structure)
        files_table <- gitstats_map(orgs, function(org) {
          owner_type <- attr(org, "type") %||% "organization"
          repos <- files_structure[[org]] |> names()
          repos_data <- private$get_repos_data(
            org = org,
            repos = repos,
            verbose = verbose
          )
          if (verbose) {
            show_message(
              host = private$host_name,
              engine = "graphql",
              scope = org,
              information = paste0("Pulling files ", cli_icons$file, " from files structure")
            )
          }
          graphql_engine$get_files_from_org(
            org = org,
            owner_type = owner_type,
            repos_data = repos_data,
            host_files_structure = files_structure,
            verbose = verbose
          ) |>
            graphql_engine$prepare_files_table(
              org = org
            )
        }, .progress = set_progress_bar(progress, private)) |>
          purrr::list_rbind() |>
          private$add_repo_api_url()
        return(files_table)
      } else {
        cli::cli_alert_warning("[GitHub] No files {cli_icons$file} found. Skipping pulling files content.")
        return(NULL)
      }
    },

    get_files_structure_from_repos = function(pattern,
                                              depth,
                                              verbose  = TRUE,
                                              progress = TRUE) {
      if ("repo" %in% private$searching_scope) {
        rest_engine <- private$engines$rest
        graphql_engine <- private$engines$graphql
        orgs <- graphql_engine$set_owner_type(
          owners = names(private$orgs_repos),
          verbose = verbose
        )
        files_structure_list <- gitstats_map(orgs, function(org) {
          repos_data <- private$get_repos_data(
            org = org,
            repos = private$orgs_repos[[org]],
            verbose = verbose
          )
          if (verbose) {
            user_info <- if (!is.null(pattern)) {
              glue::glue(
                "Pulling repos {cli_icons$tree} [files matching pattern: '{paste0(pattern, collapse = '|')}']"
              )
            } else {
              glue::glue("Pulling repos {cli_icons$tree}")
            }
            show_message(
              host = private$host_name,
              engine = "rest",
              scope = set_repo_scope(org, private),
              information = user_info
            )
          }
          private$get_files_structure_from_repos_data(
            rest_engine = rest_engine,
            org = org,
            repos_data = repos_data,
            pattern = pattern,
            depth = depth,
            verbose = verbose
          )
        }, .progress = set_progress_bar(progress, private))
        names(files_structure_list) <- orgs
        files_structure_list <- files_structure_list |>
          purrr::discard(~ length(.) == 0)
        if (length(files_structure_list) == 0 && verbose) {
          cli::cli_alert_warning(
            cli::col_yellow(
              "For {private$host_name} no files {cli_icons$tree} structure found."
            )
          )
        }
        return(files_structure_list)
      }
    },

    get_repos_data = function(org, repos = NULL, verbose) {
      cached_repos <- private$get_cached_repos(org)
      if (is.null(cached_repos)) {
        if (verbose) cli::cli_alert("[{org}] Pulling repositories {cli_icons$repo} data...")
        owner_type <- attr(org, "type") %||% "organization"
        org <- url_decode(org)
        graphql_engine <- private$engines$graphql
        repos_from_org <- graphql_engine$get_repos_from_org(
          org = org,
          owner_type = owner_type,
          verbose = verbose
        )
        private$set_cached_repos(repos_from_org, org, verbose)
        tryCatch(
          private$save_repos_to_storage(
            repos_from_org, org, graphql_engine
          ),
          error = function(e) NULL
        )
      } else {
        if (verbose) cli::cli_alert("[{org}] Using cached repositories data...")
        repos_from_org <- cached_repos
      }
      if (!is.null(repos)) {
        repos_from_org <- repos_from_org |>
          purrr::keep(~ .$repo_path %in% repos)
      }
      repos_data <- list(
        "paths" = purrr::map(repos_from_org, ~ .$repo_path),
        "def_branches" = purrr::map(repos_from_org, ~ .$default_branch$name),
        "repo_ids" = purrr::map_chr(repos_from_org, ~ .$repo_id)
      )
      return(repos_data)
    },

    save_repos_to_storage = function(repos_from_org, org, engine) {
      storage <- private$storage_backend
      if (!is.null(storage) && storage$is_db()) {
        repos_table <- engine$prepare_repos_table(repos_from_org, org)
        if (!is.null(repos_table) && nrow(repos_table) > 0) {
          repos_table <- dplyr::as_tibble(repos_table)
          existing <- storage$load("repositories")
          if (!is.null(existing)) {
            repos_table <- dplyr::bind_rows(existing, repos_table) |>
              dplyr::distinct(repo_id, .keep_all = TRUE)
          }
          class(repos_table) <- c("gitstats_repos", class(repos_table))
          storage$save("repositories", repos_table)
        }
      }
    },

    set_repo_url = function(repo_fullname) {
      paste0(private$endpoints$repositories, "/", repo_fullname)
    }
  )
)
