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);
}
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;
}
}
}
}