using Microsoft.Extensions.Logging; using TrueCV.Application.Interfaces; using TrueCV.Application.Models; namespace TrueCV.Infrastructure.Services; public sealed class TimelineAnalyserService : ITimelineAnalyserService { private readonly ILogger _logger; private const int MinimumGapMonths = 3; private const int AllowedOverlapMonths = 2; public TimelineAnalyserService(ILogger logger) { _logger = logger; } public TimelineAnalysisResult Analyse(List employmentHistory) { ArgumentNullException.ThrowIfNull(employmentHistory); if (employmentHistory.Count == 0) { _logger.LogDebug("No employment history to analyse"); return new TimelineAnalysisResult { TotalGapMonths = 0, TotalOverlapMonths = 0, Gaps = [], Overlaps = [] }; } // Filter entries with valid dates and sort by start date var sortedEmployment = employmentHistory .Where(e => e.StartDate.HasValue) .OrderBy(e => e.StartDate!.Value) .ToList(); if (sortedEmployment.Count == 0) { _logger.LogDebug("No employment entries with valid dates to analyse"); return new TimelineAnalysisResult { TotalGapMonths = 0, TotalOverlapMonths = 0, Gaps = [], Overlaps = [] }; } var gaps = DetectGaps(sortedEmployment); var overlaps = DetectOverlaps(sortedEmployment); var totalGapMonths = gaps.Sum(g => g.Months); var totalOverlapMonths = overlaps.Sum(o => o.Months); _logger.LogInformation( "Timeline analysis complete: {GapCount} gaps ({TotalGapMonths} months), {OverlapCount} overlaps ({TotalOverlapMonths} months)", gaps.Count, totalGapMonths, overlaps.Count, totalOverlapMonths); return new TimelineAnalysisResult { TotalGapMonths = totalGapMonths, TotalOverlapMonths = totalOverlapMonths, Gaps = gaps, Overlaps = overlaps }; } private List DetectGaps(List sortedEmployment) { var gaps = new List(); for (var i = 0; i < sortedEmployment.Count - 1; i++) { var current = sortedEmployment[i]; var next = sortedEmployment[i + 1]; // Get the effective end date for the current position var currentEndDate = GetEffectiveEndDate(current); var nextStartDate = next.StartDate!.Value; // Skip if there's no gap or overlap if (currentEndDate >= nextStartDate) { continue; } var gapMonths = CalculateMonthsDifference(currentEndDate, nextStartDate); // Only report gaps of 3+ months if (gapMonths >= MinimumGapMonths) { _logger.LogDebug( "Detected {Months} month gap between {EndDate} and {StartDate}", gapMonths, currentEndDate, nextStartDate); gaps.Add(new TimelineGap { StartDate = currentEndDate, EndDate = nextStartDate, Months = gapMonths }); } } return gaps; } private List DetectOverlaps(List sortedEmployment) { var overlaps = new List(); for (var i = 0; i < sortedEmployment.Count; i++) { for (var j = i + 1; j < sortedEmployment.Count; j++) { var earlier = sortedEmployment[i]; var later = sortedEmployment[j]; var overlap = CalculateOverlap(earlier, later); if (overlap is not null && overlap.Value.Months > AllowedOverlapMonths) { _logger.LogDebug( "Detected {Months} month overlap between {Company1} and {Company2}", overlap.Value.Months, earlier.CompanyName, later.CompanyName); overlaps.Add(new TimelineOverlap { Company1 = earlier.CompanyName, Company2 = later.CompanyName, OverlapStart = overlap.Value.Start, OverlapEnd = overlap.Value.End, Months = overlap.Value.Months }); } } } return overlaps; } private static (DateOnly Start, DateOnly End, int Months)? CalculateOverlap( EmploymentEntry earlier, EmploymentEntry later) { if (!earlier.StartDate.HasValue || !later.StartDate.HasValue) { return null; } var earlierEnd = GetEffectiveEndDate(earlier); var laterStart = later.StartDate.Value; // No overlap if earlier job ended before later job started if (earlierEnd <= laterStart) { return null; } var laterEnd = GetEffectiveEndDate(later); // The overlap period var overlapStart = laterStart; var overlapEnd = earlierEnd < laterEnd ? earlierEnd : laterEnd; if (overlapStart >= overlapEnd) { return null; } var months = CalculateMonthsDifference(overlapStart, overlapEnd); return (overlapStart, overlapEnd, months); } private static DateOnly GetEffectiveEndDate(EmploymentEntry entry) { if (entry.EndDate.HasValue) { return entry.EndDate.Value; } // If marked as current or no end date, use today return DateOnly.FromDateTime(DateTime.UtcNow); } private static int CalculateMonthsDifference(DateOnly startDate, DateOnly endDate) { var yearDiff = endDate.Year - startDate.Year; var monthDiff = endDate.Month - startDate.Month; var totalMonths = (yearDiff * 12) + monthDiff; // Add a month if we've passed the day in the month if (endDate.Day >= startDate.Day) { totalMonths++; } return Math.Max(0, totalMonths); } }