namespace YMhut.Box.Core.Startup;

public sealed record StartupInitializationStage(
    string Id,
    string DisplayName,
    double Weight,
    bool Critical,
    Func<StartupInitializationStageContext, CancellationToken, Task> ExecuteAsync);

public sealed record StartupInitializationStageProgress(
    string StageId,
    string StageName,
    int StepIndex,
    int StepCount,
    double Progress,
    string Detail,
    StartupCheckSeverity Severity = StartupCheckSeverity.Info,
    bool IsRepairStep = false,
    bool IsIndeterminate = false);

public sealed class StartupInitializationStageContext
{
    private readonly StartupInitializationPipeline _pipeline;
    private readonly StartupInitializationStage _stage;
    private readonly int _stepIndex;
    private readonly int _stepCount;
    private readonly double _completedWeight;
    private readonly double _totalWeight;
    private readonly IProgress<StartupInitializationStageProgress>? _progress;

    internal StartupInitializationStageContext(
        StartupInitializationPipeline pipeline,
        StartupInitializationStage stage,
        int stepIndex,
        int stepCount,
        double completedWeight,
        double totalWeight,
        IProgress<StartupInitializationStageProgress>? progress)
    {
        _pipeline = pipeline;
        _stage = stage;
        _stepIndex = stepIndex;
        _stepCount = stepCount;
        _completedWeight = completedWeight;
        _totalWeight = totalWeight;
        _progress = progress;
    }

    public void Report(
        double stageProgress,
        string detail,
        StartupCheckSeverity severity = StartupCheckSeverity.Info,
        bool isRepairStep = false,
        bool isIndeterminate = false)
    {
        var clampedStageProgress = Math.Clamp(stageProgress, 0, 1);
        var absolute = _totalWeight <= 0
            ? 100
            : ((_completedWeight + (_stage.Weight * clampedStageProgress)) / _totalWeight) * 100;
        _pipeline.Report(
            _progress,
            new StartupInitializationStageProgress(
                _stage.Id,
                _stage.DisplayName,
                _stepIndex,
                _stepCount,
                absolute,
                detail,
                severity,
                isRepairStep,
                isIndeterminate));
    }
}

public sealed class StartupInitializationPipeline
{
    private readonly IReadOnlyList<StartupInitializationStage> _stages;
    private double _lastProgress;

    public StartupInitializationPipeline(IEnumerable<StartupInitializationStage> stages)
    {
        _stages = stages
            .Where(stage => !string.IsNullOrWhiteSpace(stage.Id))
            .ToArray();

        if (_stages.Count == 0)
        {
            throw new ArgumentException("Startup initialization pipeline requires at least one stage.", nameof(stages));
        }
    }

    public IReadOnlyList<StartupInitializationStage> Stages => _stages;

    public async Task RunAsync(
        IProgress<StartupInitializationStageProgress>? progress = null,
        CancellationToken cancellationToken = default)
    {
        _lastProgress = 0;
        var totalWeight = _stages.Sum(stage => Math.Max(0.1, stage.Weight));
        var completedWeight = 0d;

        for (var index = 0; index < _stages.Count; index++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var stage = _stages[index] with { Weight = Math.Max(0.1, _stages[index].Weight) };
            var context = new StartupInitializationStageContext(
                this,
                stage,
                index + 1,
                _stages.Count,
                completedWeight,
                totalWeight,
                progress);

            context.Report(0, stage.DisplayName, isIndeterminate: false);
            try
            {
                await stage.ExecuteAsync(context, cancellationToken).ConfigureAwait(false);
            }
            catch when (!stage.Critical)
            {
                context.Report(1, stage.DisplayName, StartupCheckSeverity.Warning);
            }

            completedWeight += stage.Weight;
            context.Report(1, stage.DisplayName);
        }

        if (_lastProgress < 100)
        {
            var lastStage = _stages[^1];
            Report(
                progress,
                new StartupInitializationStageProgress(
                    lastStage.Id,
                    lastStage.DisplayName,
                    _stages.Count,
                    _stages.Count,
                    100,
                    lastStage.DisplayName));
        }
    }

    internal void Report(
        IProgress<StartupInitializationStageProgress>? progress,
        StartupInitializationStageProgress value)
    {
        var monotonicProgress = Math.Max(_lastProgress, Math.Clamp(value.Progress, 0, 100));
        _lastProgress = monotonicProgress;
        progress?.Report(value with { Progress = monotonicProgress });
    }
}
