using aeqw89.DataKeys; using Beam.Dynamic; using Beam; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; namespace Beam.Temporary.Cli { /// /// Type‑safe, staged builder that prevents callers from forgetting the mandatory steps /// (source → link selection → transformer) and surfaces operational knobs as first‑class /// methods instead of magic parameters. /// public static class DownloadBuilder { /* ──────────────────────────── Entry points ─────────────────────────── */ public static ILinkStage FromMeta(DataKey novelKey, BeamDataDictionary data) => Create(novelKey, data, SourceKind.Meta); public static ILinkStage FromText(DataKey novelKey, BeamDataDictionary data) => Create(novelKey, data, SourceKind.Text); /* ────────────────────────────── Stages ─────────────────────────────── */ public interface ILinkStage { ITransformStage WithLink(); ITransformStage WithLinkGenerator(); ILinkStage WithRange(Range range); } public interface ITransformStage { IContextStage WithTransformer(Func> factory); } public interface IContextStage { IContextStage Configure(Action> configure); IContextStage WithParallelism(int degree); IContextStage WithTimeout(TimeSpan timeout); IContextStage WithRetryReporter(IProgress reporter); DownloadEnumerable Build(); } /* ────────────────────────── Implementation ────────────────────────── */ private enum SourceKind { Meta, Text } private static ILinkStage Create(DataKey novelKey, BeamDataDictionary data, SourceKind kind) { var (source, initial) = Resolve(novelKey, data, kind); var ctxBuilder = new DownloadContextBuilder().WithLinks(Array.Empty()); // placeholder, filled later. return new LinkStage(source, initial, data, ctxBuilder); } private static (WebResource Source, State Initial) Resolve(DataKey novelKey, BeamDataDictionary data, SourceKind kind) { if (!data.Novels.TryGetValue(novelKey, out var tr)) throw new KeyNotFoundException($"Novel '{novelKey}' not found in BeamDataDictionary."); var textRecord = tr.ToRecord(data); WebResource? source; State? initial; if (kind == SourceKind.Meta) { source = textRecord.AssociatedMetaSource ?? throw new InvalidOperationException($"Meta source missing for '{novelKey}'."); initial = textRecord.Resource.MetaTemplateInitialData ?? throw new InvalidOperationException("Meta template data missing."); } else { source = textRecord.AssociatedSource ?? throw new InvalidOperationException($"Text source missing for '{novelKey}'."); initial = textRecord.Resource.TemplateInitialData; } return (source, initial); } /* ──────────────────────────── Stage types ─────────────────────────── */ private sealed record LinkStage( WebResource Source, State Initial, BeamDataDictionary Data, DownloadContextBuilder CtxBuilder) : ILinkStage { private State? endState; private bool linksFrozen = false; public ITransformStage WithLink() { var link = Data.Templates[Source.Key].Builder.Build(Initial); CtxBuilder.WithLinks(new[] { link }); return new TransformStage(Source, Data, CtxBuilder); } public ITransformStage WithLinkGenerator() { var template = Data.Templates[Source.Key]; var generator = SourceLinkEnumerable.FromGenerator(new OrderedSourceLinkGenerator( template.Builder, new NumberedStateChanger(template.Factory.Behavior), Initial, endState)); CtxBuilder.WithLinks(generator); linksFrozen = true; return new TransformStage(Source, Data, CtxBuilder); } public ILinkStage WithRange(Range range) { if (linksFrozen) throw new InvalidOperationException($"WithRange must be called before WithLinkGenerator"); if (range.End.Value < range.Start.Value) throw new ArgumentOutOfRangeException(nameof(range), $" start must be < end"); var template = Data.Templates[Source.Key]; var stateChanger = new NumberedStateChanger(template.Factory.Behavior); endState = Initial.Copy(); stateChanger.Apply(Initial, range.Start.Value - 1); stateChanger.Apply(endState, range.End.Value - 1); return this; } } private sealed record TransformStage( WebResource Source, BeamDataDictionary Data, DownloadContextBuilder CtxBuilder) : ITransformStage { public IContextStage WithTransformer(Func> factory) { var transformer = factory(Data.Bindings[Source.Bindings]); return new ContextStage(CtxBuilder, transformer); } } private sealed class ContextStage : IContextStage { private readonly DownloadContextBuilder _ctxBuilder; private readonly Func _transformer; private int _parallelism = 4; public ContextStage(DownloadContextBuilder ctxBuilder, Func transformer) { _ctxBuilder = ctxBuilder; _transformer = transformer; } public IContextStage Configure(Action> configure) { configure(_ctxBuilder); return this; } public IContextStage WithParallelism(int degree) { _parallelism = Math.Max(1, degree); return this; } public IContextStage WithTimeout(TimeSpan timeout) { _ctxBuilder.WithTimeOut(timeout); return this; } public IContextStage WithRetryReporter(IProgress reporter) { _ctxBuilder.WithRetryReporter(reporter); return this; } public DownloadEnumerable Build() { var context = _ctxBuilder.Build(); SequentialFragmentDownloader sequentialDownloader = new( context, ctx => new UnitFragmentDownloader( context.Web, _transformer, context.AsyncFailurePredicates, _parallelism, context.DownloadLogger), context.DownloadLogger); var enumerable = new DownloadEnumerable( sequentialDownloader .UnwrapFragmented()); sequentialDownloader.DisposeAsync().AsTask().Wait(); return enumerable; } } } }