using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace RealCV.Infrastructure.Clients; public sealed class GitHubApiClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; public GitHubApiClient( HttpClient httpClient, IOptions options, ILogger logger) { _httpClient = httpClient; _logger = logger; _httpClient.BaseAddress = new Uri("https://api.github.com/"); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); _httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("RealCV/1.0"); if (!string.IsNullOrEmpty(options.Value.PersonalAccessToken)) { _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.Value.PersonalAccessToken); } } public async Task GetUserAsync(string username) { try { var url = $"users/{Uri.EscapeDataString(username)}"; _logger.LogDebug("Getting GitHub user: {Username}", username); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning("GitHub API returned {StatusCode} for user: {Username}", response.StatusCode, username); return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting GitHub user: {Username}", username); return null; } } public async Task> GetUserReposAsync(string username, int perPage = 100) { var repos = new List(); var page = 1; try { while (true) { var url = $"users/{Uri.EscapeDataString(username)}/repos?per_page={perPage}&page={page}&sort=updated"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { break; } var pageRepos = await response.Content.ReadFromJsonAsync>(JsonOptions); if (pageRepos == null || pageRepos.Count == 0) { break; } repos.AddRange(pageRepos); if (pageRepos.Count < perPage) { break; } page++; // Limit to avoid rate limiting if (page > 5) { break; } } } catch (Exception ex) { _logger.LogError(ex, "Error getting repos for user: {Username}", username); } return repos; } public async Task?> GetRepoLanguagesAsync(string owner, string repo) { try { var url = $"repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/languages"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } return await response.Content.ReadFromJsonAsync>(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting languages for repo: {Owner}/{Repo}", owner, repo); return null; } } public async Task SearchUsersAsync(string query, int perPage = 30) { try { var encodedQuery = Uri.EscapeDataString(query); var url = $"search/users?q={encodedQuery}&per_page={perPage}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error searching GitHub users: {Query}", query); return null; } } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } public class GitHubOptions { public string PersonalAccessToken { get; set; } = string.Empty; } // Response models public class GitHubUser { public string? Login { get; set; } public int Id { get; set; } public string? Name { get; set; } public string? Company { get; set; } public string? Blog { get; set; } public string? Location { get; set; } public string? Email { get; set; } public string? Bio { get; set; } [JsonPropertyName("twitter_username")] public string? TwitterUsername { get; set; } [JsonPropertyName("public_repos")] public int PublicRepos { get; set; } [JsonPropertyName("public_gists")] public int PublicGists { get; set; } public int Followers { get; set; } public int Following { get; set; } [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } [JsonPropertyName("updated_at")] public DateTime UpdatedAt { get; set; } [JsonPropertyName("html_url")] public string? HtmlUrl { get; set; } [JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } } public class GitHubRepo { public int Id { get; set; } public string? Name { get; set; } [JsonPropertyName("full_name")] public string? FullName { get; set; } public string? Description { get; set; } public string? Language { get; set; } [JsonPropertyName("html_url")] public string? HtmlUrl { get; set; } public bool Fork { get; set; } public bool Private { get; set; } [JsonPropertyName("stargazers_count")] public int StargazersCount { get; set; } [JsonPropertyName("watchers_count")] public int WatchersCount { get; set; } [JsonPropertyName("forks_count")] public int ForksCount { get; set; } public int Size { get; set; } [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } [JsonPropertyName("updated_at")] public DateTime UpdatedAt { get; set; } [JsonPropertyName("pushed_at")] public DateTime? PushedAt { get; set; } } public class GitHubUserSearchResponse { [JsonPropertyName("total_count")] public int TotalCount { get; set; } [JsonPropertyName("incomplete_results")] public bool IncompleteResults { get; set; } public List? Items { get; set; } } public class GitHubUserSearchItem { public string? Login { get; set; } public int Id { get; set; } [JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } [JsonPropertyName("html_url")] public string? HtmlUrl { get; set; } public double Score { get; set; } }