namespace KArtSell.Modules.ModelOperations.Domain;
///
/// VS-02 DOMAIN: Security Master Synchronization Policy
///
/// Handles:
/// - Conflict resolution (last-write-wins)
/// - Permission rule validation
/// - Version management
/// - Idempotency keys
///
/// Pure logic, no I/O, testable, deterministic.
///
public record SecurityRule(
Guid RuleId,
string ResourceName,
string Action,
int Version,
DateTime EffectiveAt,
DateTime? ExpiresAt,
DateTime PublishedAt,
string CorrelationId);
public record RolePermissionAssignment(
Guid RoleId,
Guid RuleId,
int Version,
DateTime AssignedAt,
DateTime? RemovedAt);
public record SyncState(
int LocalVersion,
int RemoteVersion,
List LocalRules,
List RemoteRules,
string IdempotencyKey,
string CorrelationId);
public record SyncResult(
bool IsSuccess,
int NewVersion,
List AppliedRules,
List Conflicts,
string? ErrorMessage,
string CorrelationId);
public static class SecurityMasterPolicy
{
///
/// Determine sync action: accept, reject, or rollback
///
/// Rules:
/// 1. If localVersion >= remoteVersion: Already synced (idempotent)
/// 2. If localVersion < remoteVersion: Accept all remote rules
/// 3. Version conflict: Reject with 409
/// 4. Last-write-wins per rule (by PublishedAt timestamp)
///
public static SyncResult ResolveSyncConflict(SyncState state)
{
if (state.LocalVersion > state.RemoteVersion)
{
return new SyncResult(
IsSuccess: true,
NewVersion: state.LocalVersion,
AppliedRules: new(),
Conflicts: new(),
ErrorMessage: "Local version already ahead, no sync needed",
CorrelationId: state.CorrelationId);
}
if (state.LocalVersion == state.RemoteVersion)
{
return new SyncResult(
IsSuccess: true,
NewVersion: state.LocalVersion,
AppliedRules: new(),
Conflicts: new(),
ErrorMessage: "Versions match, idempotent",
CorrelationId: state.CorrelationId);
}
var conflicts = new List();
var rulesToApply = new List();
foreach (var remoteRule in state.RemoteRules)
{
var localRule = state.LocalRules.FirstOrDefault(r => r.RuleId == remoteRule.RuleId);
if (localRule == null)
{
rulesToApply.Add(remoteRule);
continue;
}
if (localRule.PublishedAt < remoteRule.PublishedAt)
{
rulesToApply.Add(remoteRule);
}
else if (localRule.PublishedAt == remoteRule.PublishedAt && localRule.Version < remoteRule.Version)
{
rulesToApply.Add(remoteRule);
conflicts.Add($"Version conflict on rule {remoteRule.RuleId}: local {localRule.Version}, remote {remoteRule.Version}");
}
}
return new SyncResult(
IsSuccess: true,
NewVersion: state.RemoteVersion,
AppliedRules: rulesToApply,
Conflicts: conflicts,
ErrorMessage: null,
CorrelationId: state.CorrelationId);
}
///
/// Validate rule before applying
///
/// Checks:
/// - Resource name not empty
/// - Action in {read, write, execute}
/// - EffectiveAt <= ExpiresAt (if set)
/// - Timestamps in UTC
///
public static (bool IsValid, List Errors) ValidateRule(SecurityRule rule)
{
var errors = new List();
if (string.IsNullOrWhiteSpace(rule.ResourceName))
errors.Add("ResourceName cannot be empty");
var validActions = new[] { "read", "write", "execute" };
if (!validActions.Contains(rule.Action.ToLowerInvariant()))
errors.Add($"Action must be one of: {string.Join(", ", validActions)}");
if (rule.ExpiresAt.HasValue && rule.EffectiveAt > rule.ExpiresAt)
errors.Add("EffectiveAt must be before or equal to ExpiresAt");
if (rule.PublishedAt.Kind != DateTimeKind.Utc)
errors.Add("PublishedAt must be UTC");
return (errors.Count == 0, errors);
}
///
/// Check if rule is active at given time
///
public static bool IsRuleActive(SecurityRule rule, DateTime? asOf = null)
{
var now = asOf ?? DateTime.UtcNow;
if (now < rule.EffectiveAt)
return false;
if (rule.ExpiresAt.HasValue && now > rule.ExpiresAt)
return false;
return true;
}
///
/// Create idempotency key for sync operation
/// Format: {fromVersion}:{correlationId}
///
public static string CreateIdempotencyKey(int fromVersion, string correlationId)
{
return $"sync-{fromVersion}-{correlationId}";
}
///
/// Detect rollback scenario: partial sync that failed mid-transaction
///
/// If applied rules don't match version increment, rollback needed.
///
public static bool RequiresRollback(int appliedRuleCount, int versionIncrement)
{
return appliedRuleCount == 0 && versionIncrement > 0;
}
}