using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
using ZB.MOM.WW.ScadaBridge.KpiHistory;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;

/// <summary>
/// Default <see cref="IKpiHistoryQueryService"/> implementation — fetches
/// the raw series via <see cref="IKpiHistoryRepository.GetRawSeriesAsync"/> and
/// reduces it with <see cref="KpiSeriesBucketer.Bucket"/> to at most the requested
/// (or configured-default) number of points.
/// </summary>
/// <remarks>
/// <para>
/// Mirrors <see cref="AuditLogQueryService"/>: the production constructor takes an
/// <see cref="IServiceScopeFactory"/> and opens a fresh DI scope per query —
/// resolving a fresh <see cref="IKpiHistoryRepository"/> (and therefore a fresh
/// <c>ScadaBridgeDbContext</c>) — so a trend chart's auto-load never contends with
/// the circuit-scoped context the rest of the page uses.
/// </para>
/// <para>
/// A second constructor injects an <see cref="IKpiHistoryRepository"/> directly — a
/// test seam (same dual-ctor pattern as <see cref="AuditLogQueryService"/>) so unit
/// tests can substitute a stub without standing up a DI container. Both ctors take
/// <see cref="IOptions{KpiHistoryOptions}"/> for the default series-point ceiling.
/// </para>
/// </remarks>
public sealed class KpiHistoryQueryService : IKpiHistoryQueryService
{
    // Production path: open a fresh scope per operation. Null in the test-seam ctor.
    private readonly IServiceScopeFactory? _scopeFactory;

    // Test seam: a directly-injected repository whose lifetime the test owns.
    // Null in the production ctor.
    private readonly IKpiHistoryRepository? _injectedRepository;

    private readonly KpiHistoryOptions _options;

    /// <summary>
    /// Production constructor — resolves <see cref="IKpiHistoryRepository"/> from a
    /// fresh DI scope on every call so each query gets its own
    /// <c>ScadaBridgeDbContext</c> and never contends with the circuit-scoped context.
    /// </summary>
    /// <param name="scopeFactory">Factory used to open a fresh DI scope per query.</param>
    /// <param name="options">KPI History options supplying the default series-point ceiling.</param>
    public KpiHistoryQueryService(
        IServiceScopeFactory scopeFactory,
        IOptions<KpiHistoryOptions> options)
    {
        _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
    }

    /// <summary>
    /// Test-seam constructor — injects a repository instance whose lifetime the
    /// caller owns. Used by unit tests that substitute a stub repository.
    /// </summary>
    /// <param name="repository">The KPI history repository instance to use directly.</param>
    /// <param name="options">KPI History options supplying the default series-point ceiling.</param>
    public KpiHistoryQueryService(
        IKpiHistoryRepository repository,
        IOptions<KpiHistoryOptions> options)
    {
        _injectedRepository = repository ?? throw new ArgumentNullException(nameof(repository));
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<KpiSeriesPoint>> GetSeriesAsync(
        string source, string metric, string scope, string? scopeKey,
        DateTime fromUtc, DateTime toUtc, int? maxPoints = null, CancellationToken cancellationToken = default)
    {
        var effectiveMax = maxPoints ?? _options.DefaultMaxSeriesPoints;

        // Per-metric reduction intent: the same catalog that drives the hourly fold drives
        // the chart-time bucket reduction, so a Rate series is summed per bucket on BOTH the
        // raw-minute and hourly-rollup routes — one unit ("events per chart bucket") on both
        // sides of the RollupThresholdHours routing boundary (arch-review 04 round 2, R3).
        var aggregation = KpiMetricAggregationCatalog.Resolve(source, metric);

        // Test-seam ctor: use the injected repository directly.
        if (_injectedRepository is not null)
        {
            var injectedSeries = await FetchSeriesAsync(
                _injectedRepository, source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken);
            return KpiSeriesBucketer.Bucket(injectedSeries, fromUtc, toUtc, effectiveMax, aggregation);
        }

        // Production: a fresh scope (and thus a fresh DbContext) per query so a
        // chart's auto-load never shares the circuit-scoped context.
        await using var serviceScope = _scopeFactory!.CreateAsyncScope();
        var repository = serviceScope.ServiceProvider.GetRequiredService<IKpiHistoryRepository>();
        var series = await FetchSeriesAsync(
            repository, source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken);
        return KpiSeriesBucketer.Bucket(series, fromUtc, toUtc, effectiveMax, aggregation);
    }

    /// <summary>
    /// Routes a series fetch to the raw-sample or hourly-rollup repository method by
    /// the requested window width. Windows wider than
    /// <see cref="KpiHistoryOptions.RollupThresholdHours"/> read the pre-aggregated
    /// hourly rollups (≈60× fewer rows scanned on 30 d/90 d ranges); windows at or below
    /// the threshold read raw samples, preserving intra-minute detail on the 24 h/7 d
    /// charts. Both paths return the same ascending <see cref="KpiSeriesPoint"/> shape, so
    /// the caller's <see cref="KpiSeriesBucketer.Bucket"/> step is agnostic to the source
    /// (a 90 d rollup can still exceed the point ceiling, so bucketing still applies).
    /// The boundary is strict: exactly <c>RollupThresholdHours</c> stays on the raw path.
    /// <para>
    /// The routing boundary no longer changes a Rate chart's magnitude: both paths reduce to
    /// per-bucket sums via the catalog-driven aggregation applied in
    /// <see cref="GetSeriesAsync"/>. A residual ≤~1.2× native-resolution difference exists only
    /// for an unbucketed just-over-threshold rollup series (plotted at native per-hour sums) vs.
    /// an at-threshold bucketed raw series — documented, not hidden (arch-review 04 round 2, R3).
    /// </para>
    /// </summary>
    private Task<IReadOnlyList<KpiSeriesPoint>> FetchSeriesAsync(
        IKpiHistoryRepository repository,
        string source, string metric, string scope, string? scopeKey,
        DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken)
    {
        var windowHours = (toUtc - fromUtc).TotalHours;
        return windowHours > _options.RollupThresholdHours
            ? repository.GetHourlySeriesAsync(source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken)
            : repository.GetRawSeriesAsync(source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken);
    }
}
