using Microsoft.Extensions.Configuration;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;

/// <summary>
/// Tuning knobs for the central <see cref="AuditLogPurgeActor"/> singleton.
/// Default cadence is 24 hours; the retention window itself
/// is sourced from <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions.RetentionDays"/>
/// (default 365) so operators tune retention from a single section.
/// </summary>
/// <remarks>
/// <para>
/// The purge actor is a daily-cadence singleton, not a hot-loop. The
/// partition switch itself is metadata-only (since WP2.2's
/// <c>AlignAuditLogEventIdUniqueness</c> it no longer drops and rebuilds a
/// non-aligned unique index around it), but it still takes schema-modification
/// locks and competes with ingest for the same table. Lower this only when an
/// operator can prove they need sub-daily purge granularity.
/// </para>
/// <para>
/// <see cref="IntervalOverride"/> exists for tests to drop the cadence to
/// milliseconds; production config is expected to set <see cref="IntervalHours"/>
/// only. Because this options class is <c>Bind</c>-ed wholesale, a config value
/// at <c>AuditLog:Purge:IntervalOverride</c> would bind if present (and would
/// bypass the <see cref="Interval"/> minimum clamp) — operators must not set it.
/// </para>
/// </remarks>
public sealed class AuditLogPurgeOptions
{
    /// <summary>Period of the purge tick in hours (default 24).</summary>
    public int IntervalHours { get; set; } = 24;

    /// <summary>
    /// Batch size for the per-channel retention-override row DELETE
    /// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.PurgeChannelOlderThanAsync"/>).
    /// Each <c>DELETE TOP (@batch)</c> caps the transaction-log and lock footprint
    /// per statement; the repository loops batches until no rows remain. Default
    /// 5000 keeps individual deletes short on a busy central DB while still draining
    /// a large backlog within a tick. Clamped to a sane minimum in
    /// <see cref="ChannelPurgeBatchSize"/>.
    /// </summary>
    /// <remarks>
    /// The operator-facing config key is <c>ChannelPurgeBatchSize</c>
    /// (per Component-AuditLog.md), so the binder maps that documented key onto this
    /// backing property via <see cref="ConfigurationKeyNameAttribute"/>. The unattributed
    /// property name (<c>ChannelPurgeBatchSizeConfigured</c>) would otherwise have been
    /// the bind key, silently ignoring the documented section.
    /// </remarks>
    [ConfigurationKeyName("ChannelPurgeBatchSize")]
    public int ChannelPurgeBatchSizeConfigured { get; set; } = 5000;

    /// <summary>
    /// Resolves the effective per-channel purge batch size, clamped to at least 1 so
    /// a misconfigured <c>0</c>/negative value cannot make the repository's DELETE
    /// loop spin or throw.
    /// </summary>
    public int ChannelPurgeBatchSize => ChannelPurgeBatchSizeConfigured < 1 ? 1 : ChannelPurgeBatchSizeConfigured;

    /// <summary>
    /// Per-command timeout (in minutes) for the maintenance SQL the purge tick issues —
    /// both the partition switch-out staging batch
    /// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.SwitchOutPartitionAsync"/>)
    /// and each per-channel <c>DELETE TOP</c> batch. Default 30 minutes.
    /// </summary>
    /// <remarks>
    /// The ADO.NET default command timeout is ~30 seconds. On a large or contended partition the
    /// SWITCH batch can exceed that and abort mid-flight, leaving an orphaned staging table for the
    /// next tick's CATCH branch to clean up (arch-review 04, S2). A generous maintenance timeout
    /// lets the metadata-only switch complete rather than self-locking. Resolved via
    /// <see cref="ResolvedMaintenanceCommandTimeout"/>, clamped to a 1-minute floor.
    /// </remarks>
    public int MaintenanceCommandTimeoutMinutes { get; set; } = 30;

    /// <summary>
    /// Resolves the effective maintenance command timeout, clamped to at least 1 minute so a
    /// misconfigured <c>0</c>/negative value cannot yield a zero/negative timeout (which ADO.NET
    /// interprets as "no timeout" for <c>0</c> and rejects for negatives).
    /// </summary>
    public TimeSpan ResolvedMaintenanceCommandTimeout =>
        TimeSpan.FromMinutes(MaintenanceCommandTimeoutMinutes < 1 ? 1 : MaintenanceCommandTimeoutMinutes);

    /// <summary>
    /// Test-only override for finer control over the tick cadence than
    /// whole-hour resolution allows. When non-null, takes precedence over
    /// <see cref="IntervalHours"/> AND bypasses the <see cref="Interval"/>
    /// minimum clamp (so tests can use millisecond cadences). Production
    /// config exposes <see cref="IntervalHours"/> only and never sets this
    /// knob — but because the options class is <c>Bind</c>-ed wholesale, a
    /// config value at <c>AuditLog:Purge:IntervalOverride</c> WOULD bind if
    /// present; operators must not set it.
    /// </summary>
    public TimeSpan? IntervalOverride { get; set; }

    /// <summary>
    /// Minimum interval the config-bound <see cref="IntervalHours"/> can
    /// resolve to. Clamps a misconfigured <c>IntervalHours: 0</c> (or a
    /// negative value) away from <see cref="TimeSpan.Zero"/> — a zero
    /// interval would make Akka's <c>ScheduleTellRepeatedlyCancelable</c>
    /// spin, looping the partition drop/rebuild dance into a sustained SQL
    /// outage. The test-only <see cref="IntervalOverride"/> bypasses this
    /// clamp so unit tests can still drop the cadence to milliseconds.
    /// </summary>
    private static readonly TimeSpan MinConfiguredInterval = TimeSpan.FromMinutes(1);

    /// <summary>
    /// Resolves the effective tick interval, honouring the test override
    /// when set. Falls back to <see cref="IntervalHours"/>, clamped to at
    /// least <see cref="MinConfiguredInterval"/> so a zero/negative config
    /// value can never yield <see cref="TimeSpan.Zero"/> (which would spin
    /// the scheduler).
    /// </summary>
    public TimeSpan Interval
    {
        get
        {
            if (IntervalOverride is { } overrideValue)
            {
                return overrideValue;
            }

            var resolved = TimeSpan.FromHours(IntervalHours);
            return resolved < MinConfiguredInterval ? MinConfiguredInterval : resolved;
        }
    }
}
