using Microsoft.Extensions.Logging; using System.Net.Http; using static BlueMine.Redmine.RedmineDto; namespace BlueMine.Redmine { public class RedmineManager : IRedmineManager { private readonly RedmineConnect _redmineConnect; private readonly AsyncLock _lock = new(); private readonly RedmineCache _projectCache; private readonly RedmineCache _issueCache; private readonly ILogger _logger; public RedmineManager(HttpClient httpClient, ILoggerFactory loggerFactory, RedmineConfig config) { ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(loggerFactory); ArgumentNullException.ThrowIfNull(config); _logger = loggerFactory.CreateLogger(); _logger.LogDebug("Initializing RedmineManager with URL: {Url}", config.RedmineUrl); _redmineConnect = new RedmineConnect(httpClient, loggerFactory.CreateLogger(), config); _projectCache = new(config.ProjectCacheDuration, loggerFactory.CreateLogger>()); _issueCache = new(config.IssueCacheDuration, loggerFactory.CreateLogger>()); _logger.LogDebug("RedmineManager initialized"); } /// /// Checks if the Redmine instance is available by verifying the API key. /// /// True if available, false otherwise. public async Task IsRedmineAvailable() { _logger.LogDebug("Checking if Redmine is available"); try { using (await _lock.LockAsync()) { await _redmineConnect.VerifyApiKey(); } _logger.LogDebug("Redmine is available"); return true; } catch (Exception ex) { _logger.LogDebug("Redmine not available: {Message}", ex.Message); return false; } } /// /// Logs time for a specific issue. /// /// The issue ID. /// Hours to log. /// Comments for the time entry. /// Date of the time entry. /// Optional activity ID. public async Task LogTimeAsync(int issueId, double hours, string comments, DateTime date, int? activityId = null) { _logger.LogDebug("Logging {Hours} hours for issue {IssueId} on {Date}", hours, issueId, date.ToShortDateString()); using (await _lock.LockAsync()) { await _redmineConnect.LogTimeAsync(issueId, hours, comments, date, activityId); } _logger.LogDebug("Time logged successfully"); } /// /// Closes the specified issue. /// /// The issue ID to close. public async Task CloseIssueAsync(int issueId) { _logger.LogDebug("Closing issue {IssueId}", issueId); using (await _lock.LockAsync()) { await _redmineConnect.CloseIssueAsync(issueId); } _logger.LogDebug("Issue {IssueId} closed", issueId); } /// /// Creates a new issue in the specified project. /// /// The project ID. /// The tracker ID. /// Issue subject. /// Optional description. /// Optional estimated hours. /// Optional priority ID. /// Optional parent issue ID. /// The created issue ID. public async Task CreateIssueAsync(string projectId, int trackerId, string subject, string? description = null, double? estimatedHours = null, int? priorityId = 9, int? parentIssueId = null) { _logger.LogDebug("Creating issue in project {ProjectId} with subject '{Subject}'", projectId, subject); using (await _lock.LockAsync()) { var issueId = await _redmineConnect.CreateIssueAsync(projectId, trackerId, subject, description, estimatedHours, priorityId, parentIssueId); _logger.LogDebug("Issue created with ID {IssueId}", issueId); return issueId; } } /// /// Retrieves the list of projects, using cache if valid. /// /// Maximum number of projects to fetch per request. /// Optional progress reporter. /// List of simple projects. public async Task> GetProjectsAsync(int limit = 100, IProgress<(int, int)>? progress = null) { _logger.LogDebug("Getting projects"); using (await _lock.LockAsync()) { List projects = []; if(!_projectCache.IsCacheValid()) { _logger.LogDebug("Cache invalid, refreshing"); _projectCache.RefreshCache([..(await _redmineConnect.GetProjectsAsync(limit, progress))]); } else { _logger.LogDebug("Using cached projects"); } projects = _projectCache.GetItems(); _logger.LogDebug("Retrieved {Count} projects", projects.Count); return projects; } } /// /// Retrieves trackers for the specified project. /// /// The project ID. /// Optional cancellation token. /// List of simple trackers. public async Task> GetTrackersAsync(string projectId, CancellationToken? token = null) { _logger.LogDebug("Getting trackers for project {ProjectId}", projectId); try { using (await _lock.LockAsync()) { var trackers = await _redmineConnect.GetTrackersAsync(projectId, token); _logger.LogDebug("Retrieved {Count} trackers", trackers.Count); return trackers; } } catch (OperationCanceledException) { _logger.LogDebug("GetTrackersAsync cancelled"); throw; } } /// /// Retrieves current issues with spent time. /// /// Optional progress reporter. /// List of issue items. public async Task> GetCurrentIssuesAsync(IProgress<(int, int)>? progress = null) { _logger.LogDebug("Getting current issues"); using (await _lock.LockAsync()) { List simpleIssues; if (!_issueCache.IsCacheValid()) { _logger.LogDebug("Issue cache invalid, refreshing"); simpleIssues = [.. (await _redmineConnect.GetMyIssuesAsync())]; _issueCache.RefreshCache(simpleIssues); } else { _logger.LogDebug("Using cached issues"); simpleIssues = _issueCache.GetItems(); } var issues = await _redmineConnect.GetSpentTimeForIssuesAsync(simpleIssues, progress); _logger.LogDebug("Retrieved {Count} issues", issues.Count); return issues; } } /// /// Retrieves logged hours for the specified date range. /// /// Start date. /// End date. /// Total logged hours. public async Task GetLoggedHoursAsync(DateTime? startDate = null, DateTime? endDate = null) { var start = DateTime.Today; var end = DateTime.Today; if (startDate.HasValue) start = startDate.Value; if(endDate.HasValue) end = endDate.Value; _logger.LogDebug("Getting logged hours from {Start} to {End}", start.ToShortDateString(), end.ToShortDateString()); using (await _lock.LockAsync()) { var hours = await _redmineConnect.GetTodaysHoursAsync(start, end); _logger.LogDebug("Retrieved {Hours} hours", hours); return hours; } } } }