using System.Xml; using System.Xml.Linq; using System.Text.Json; using Launcher.Next.Core; namespace Launcher.Next.Application; public enum RosterSource { ProjectRosterJson, LegacyDownloadListXml, CentralDefaultRoster, None } public sealed record RosterPackage(string Id, string Version, string Channel, bool Enabled, int LoadOrder, bool AllowedForMachine = true); public sealed record RosterContext(string Product, int ProductYear, string Architecture, bool AllowOfflineFallback, string LegacyRosterId); public sealed record NormalizedRoster(string RosterId, string Product, int Year, string Architecture, bool AllowOfflineFallback, IReadOnlyList Packages); public sealed record RosterSelection(RosterSource Source, NormalizedRoster? Roster, string Code); public sealed class RosterAdapter { public NormalizedRoster ParseJson(string json) { using var doc=JsonDocument.Parse(json); var r=doc.RootElement; if (!r.TryGetProperty("schemaVersion",out var s)||s.GetInt32()!=1) throw new InvalidOperationException("ROSTER_SCHEMA_INVALID"); string V(string n)=>r.TryGetProperty(n,out var x)&&!string.IsNullOrWhiteSpace(x.GetString())?x.GetString()!:throw new InvalidOperationException("ROSTER_FIELD_REQUIRED:"+n); var packages=r.GetProperty("packages").EnumerateArray().Select(x=>new RosterPackage(x.GetProperty("id").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_ID_REQUIRED"),x.GetProperty("version").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_VERSION_REQUIRED"),x.GetProperty("channel").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_CHANNEL_REQUIRED"),!x.TryGetProperty("enabled",out var e)||e.GetBoolean(),x.TryGetProperty("loadOrder",out var o)?o.GetInt32():0)).ToArray(); if(packages.GroupBy(x=>x.Id,StringComparer.OrdinalIgnoreCase).Any(x=>x.Count()>1)) throw new InvalidOperationException("ROSTER_DUPLICATE_PACKAGE"); return new(V("rosterId"),V("product"),r.GetProperty("year").GetInt32(),V("architecture"),r.TryGetProperty("offlinePolicy",out var p)&&p.GetString()=="AllowVerifiedLastKnownGood",packages.OrderBy(x=>x.LoadOrder).ThenBy(x=>x.Id,StringComparer.Ordinal).ToArray()); } public NormalizedRoster ParseLegacyDownloadList(string xml, RosterContext context) { var settings=new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null,MaxCharactersInDocument=1_000_000}; using var reader=XmlReader.Create(new StringReader(xml),settings); var doc=XDocument.Load(reader); var packages=doc.Descendants("Plugin").Select((x,i)=>new RosterPackage((string?)x.Attribute("id")??throw new InvalidOperationException("LEGACY_PACKAGE_ID_REQUIRED"),(string?)x.Attribute("version")??throw new InvalidOperationException("AMBIGUOUS_LEGACY_SEMANTICS"),(string?)x.Attribute("channel")??"pilot",!string.Equals((string?)x.Attribute("install"),"false",StringComparison.OrdinalIgnoreCase),i)).ToArray(); if(packages.GroupBy(x=>x.Id,StringComparer.OrdinalIgnoreCase).Any(x=>x.Count()>1)) throw new InvalidOperationException("ROSTER_DUPLICATE_PACKAGE"); return new(context.LegacyRosterId,context.Product,context.ProductYear,context.Architecture,context.AllowOfflineFallback,packages); } public RosterSelection Select(string? projectJson,string? legacyXml,string? centralJson,RosterContext context) { if(projectJson is not null)return new(RosterSource.ProjectRosterJson,ParseJson(projectJson),"PROJECT_ROSTER_JSON"); if(legacyXml is not null)return new(RosterSource.LegacyDownloadListXml,ParseLegacyDownloadList(legacyXml,context),"LEGACY_DOWNLOAD_LIST_XML"); if(centralJson is not null)return new(RosterSource.CentralDefaultRoster,ParseJson(centralJson),"CENTRAL_DEFAULT_ROSTER"); return new(RosterSource.None,null,"NO_ROSTER_AVAILABLE"); } public IReadOnlyList Requirements(NormalizedRoster roster)=>roster.Packages.Where(x=>x.Enabled&&x.AllowedForMachine).Select(x=>new PackageRequirement(x.Id,roster.Product,roster.Year,roster.Architecture,x.Channel,x.Version,roster.RosterId,roster.AllowOfflineFallback)).ToArray(); } public sealed record PackagePlanResult(bool IsComplete,IReadOnlyList Packages,IReadOnlyList Evidence); public sealed class RosterPackagePlanUseCase(RosterAdapter rosterAdapter,PackageResolutionUseCase packageResolution) { public async Task ResolveAsync(RosterSelection selection,CancellationToken cancellationToken=default) { var evidence=new List(); if(selection.Roster is null){evidence.Add(new("PACKAGE_PLAN_BLOCKED","NO_ROSTER_AVAILABLE"));return new(false,Array.Empty(),evidence);} evidence.Add(new("ROSTER_SOURCE_SELECTED",$"{selection.Code}:{selection.Roster.RosterId}")); evidence.Add(new("ROSTER_PARSED",selection.Roster.RosterId)); evidence.Add(new("ROSTER_VALIDATED",selection.Roster.RosterId)); var requirements=rosterAdapter.Requirements(selection.Roster); if(requirements.Count==0){evidence.Add(new("PACKAGE_PLAN_BLOCKED","NO_ENABLED_PACKAGE_REQUIREMENTS"));return new(false,Array.Empty(),evidence);} var results=new List(); foreach(var requirement in requirements) { evidence.Add(new("PACKAGE_REQUIREMENT_CREATED",$"{requirement.PackageId}:{requirement.RosterIdentity}")); var result=await packageResolution.ResolveAsync(requirement,cancellationToken); results.Add(result);evidence.AddRange(result.Evidence); if(!result.IsResolved){evidence.Add(new("PACKAGE_PLAN_BLOCKED",$"PACKAGE_REQUIREMENT_REJECTED:{requirement.PackageId}"));return new(false,results,evidence);} } evidence.Add(new("PACKAGE_PLAN_COMPLETED",selection.Roster.RosterId)); return new(true,results,evidence); } }