using FluentValidation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; using System.Reflection; namespace SeaHaven.Services.DependencyInjection { public static class ServicesModule { private static readonly List ExcludedTypes = new(); public static IServiceCollection AddBusinessServices(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration); services.Configure(configuration.GetSection(JwtOptions.SectionName)); services.Configure(configuration.GetSection(ApprovalsOptions.SectionName)); services.Configure(configuration.GetSection(VendorPortalOptions.SectionName)); services.Configure(configuration.GetSection(VendorDocumentsOptions.SectionName)); services.Configure(configuration.GetSection(AccountOwnerOptions.SectionName)); services.AddOptions() .Bind(configuration.GetSection(WorkOrderWebhookOptions.SectionName)) .Validate( o => o.MaxBodyBytes is > 0 and <= 1_048_576 && o.AllowedClockSkewSeconds is >= 0 and <= 3600 && o.SecretCacheSeconds is > 0 and <= 300, "WorkOrderWebhook size, clock-skew, and cache settings are out of range.") .Validate( o => !o.Enabled || !string.IsNullOrWhiteSpace(o.SecretId), "WorkOrderWebhook requires a non-empty SecretId when enabled.") .ValidateOnStart(); services.AddOptions() .Bind(configuration.GetSection(WorkOrderReconciliationOptions.SectionName)) .Validate( o => !o.Enabled || (o.BaseUrl == "https://procurement-api.seahaven.com" && o.Region == "us-east-1" && o.PageSize is >= 1 and <= 500 && o.MaxPages is >= 1 and <= 100_000 && o.MaxCursorLength is >= 1 and <= 16_384 && o.RequestTimeoutSeconds is >= 1 and <= 120 && o.MaxRetries is >= 0 and <= 8 && o.RetryBaseDelayMilliseconds is >= 0 and <= 10_000 && o.MaxResponseBytes is >= 1 and <= 16_777_216 && o.PollSeconds is >= 1 and <= 300 && o.ScheduleMinutes is >= 1 and <= 10_080 && o.LeaseSeconds is >= 30 and <= 3_600 && HasSufficientReconciliationLease(o)), "WorkOrderReconciliation configuration is invalid.") .ValidateOnStart(); var assembly = Assembly.GetExecutingAssembly(); var allClasses = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && !ExcludedTypes.Contains(t)) .ToList(); foreach (var implementationType in allClasses) { var defaultInterface = implementationType.GetInterfaces() .FirstOrDefault(i => i.Name == $"I{implementationType.Name}"); if (defaultInterface != null) { services.AddScoped(defaultInterface, implementationType); } } services.AddValidatorsFromAssembly(assembly); // Process-wide counters: a scoped instance would start empty on every request. services.AddSingleton(); services.AddScoped( sp => (IWorkOrderReconciliationRunner)sp.GetRequiredService()); return services; } private static bool HasSufficientReconciliationLease(WorkOrderReconciliationOptions options) { try { var attempts = checked((long)options.MaxRetries + 1); var requestBudgetMilliseconds = checked( attempts * options.RequestTimeoutSeconds * 1_000L); var retryDelayMultiplier = checked((1L << options.MaxRetries) - 1L); var retryDelayMilliseconds = checked( retryDelayMultiplier * options.RetryBaseDelayMilliseconds); var worstCaseMilliseconds = checked( requestBudgetMilliseconds + retryDelayMilliseconds); return checked((long)options.LeaseSeconds * 1_000L) > worstCaseMilliseconds; } catch (OverflowException) { return false; } } } }