using aeqw89.DataKeys; using Beam.Dynamic; using Beam; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using HtmlAgilityPack; 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); public static IAlternativeLinkStage FromScratch() => new LinkStage(null!, null!, null!, new()); /* ────────────────────────────── Stages ─────────────────────────────── */ public interface ILinkStage { ITransformStage WithLink(); ITransformStage WithLinkGenerator(); ILinkStage WithRange(Range range); } public interface IAlternativeLinkStage { IAlternativeTransformStage WithLinks(IEnumerable links); } public interface ITransformStage { IContextStage WithTransformer(Func> factory); } public interface IAlternativeTransformStage { IContextStage WithTransformer(AsyncTransformer transformer); IContextStage WithTransformer(Func transformer) { return WithTransformer(rt => Task.FromResult(transformer(rt))); } } public interface IContextStage { IContextStage Configure(Action> configure); IContextStage WithParallelism(int degree); IContextStage WithTimeout(TimeSpan timeout); IContextStage WithRetryReporter(IProgress reporter); DownloadEnumerable Build(); IContextStage UseFragments(); } /* ────────────────────────── 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, IAlternativeLinkStage { 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 IAlternativeTransformStage WithLinks(IEnumerable links) { CtxBuilder.WithLinks(links); 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, IAlternativeTransformStage { public IContextStage WithTransformer(Func> factory) { var transformer = factory(Data.Bindings[Source.Bindings]); return new ContextStage(CtxBuilder, transformer); } public IContextStage WithTransformer(AsyncTransformer transformer) { return new ContextStage(CtxBuilder, transformer); } } private sealed class ContextStage : IContextStage { private readonly DownloadContextBuilder _ctxBuilder; private readonly AsyncTransformer _transformer; private int _parallelism = 4; private bool useFragments = false; public ContextStage(DownloadContextBuilder ctxBuilder, AsyncTransformer 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 IContextStage UseFragments() { useFragments = true; return this; } private object ConstructUnitDownloader(DownloadContext context) { return (useFragments, _transformer, context.AsyncFailurePredicates) switch { // ──────────────── fragmented HTML ──────────────── (true, AsyncTransformer asyncHtmlTransformer, AsyncDownloadFailurePredicate[] documentFailurePredicates) => new UnitFragmentDownloader( context.Web, asyncHtmlTransformer, documentFailurePredicates, _parallelism, context.DownloadLogger), // ──────────────── fragmented binary ──────────────── (true, AsyncTransformer asyncBinaryTransformer, AsyncDownloadFailurePredicate[] responseFailurePredicates) => new UnitFragmentDownloaderBinary( context.Client, asyncBinaryTransformer, responseFailurePredicates, _parallelism, context.DownloadLogger), // ──────────────── single HTML ──────────────── (false, AsyncTransformer asyncHtmlTransformer, AsyncDownloadFailurePredicate[] documentFailurePredicates) => new UnitDownloader( context.Web, asyncHtmlTransformer, documentFailurePredicates), // ──────────────── single binary ──────────────── (false, AsyncTransformer asyncBinaryTransformer, AsyncDownloadFailurePredicate[] responseFailurePredicates) => new UnitDownloaderBinary( context.Client, asyncBinaryTransformer, responseFailurePredicates), _ => throw new Exception($"Unsupported transformer / failure-predicate combination. Missing pattern: {useFragments} , {_transformer.GetType().AsUniqueName()} , {context.AsyncFailurePredicates?.GetType().AsUniqueName()}"), }; } private IAsyncEnumerator> ConstructDownloader(DownloadContext context) { var copyOfContext = context.CreateBuilder().Build(); return useFragments switch { true => new SequentialFragmentDownloader( copyOfContext, ctx => (IUnitDownloader>>)ConstructUnitDownloader(ctx), context.DownloadLogger).UnwrapFragmented(), false => new SequentialDownloader( copyOfContext, ctx => (IUnitDownloader)ConstructUnitDownloader(ctx), context.DownloadLogger).WrapOrdered() }; } public DownloadEnumerable Build() { var context = _ctxBuilder.Build(); var enumerable = new DownloadEnumerable(ConstructDownloader(context)); return enumerable; } } } }