using Microsoft.Extensions.Options;

namespace Mbproxy.Options;

public sealed class MbproxyOptions
{
    public BcdTagListOptions BcdTags { get; init; } = new();
    public IReadOnlyList<PlcOptions> Plcs { get; init; } = [];
    public int AdminPort { get; init; } = 8080;

    /// <summary>
    /// Server-push cadence (milliseconds) for the admin dashboard's SignalR feed.
    /// Every interval the admin endpoint builds a status snapshot and pushes it to
    /// connected dashboard / detail-page clients. Must be &gt; 0. Defaults to 1000.
    /// </summary>
    public int AdminPushIntervalMs { get; init; } = 1000;

    public ConnectionOptions Connection { get; init; } = new();
    public ResilienceOptions Resilience { get; init; } = new();

    /// <summary>
    /// Service-wide response-cache settings. The cache is opt-in per-tag
    /// (<see cref="BcdTagOptions.CacheTtlMs"/>); this section configures the safety
    /// knobs that gate / bound the cache.
    /// </summary>
    public CacheOptions Cache { get; init; } = new();
}

/// <summary>
/// Service-wide response-cache knobs. The cache is OFF by default for every tag;
/// this section governs the limits when an operator opts a tag in.
/// </summary>
public sealed class CacheOptions
{
    /// <summary>
    /// Gate for any <see cref="BcdTagOptions.CacheTtlMs"/> greater than 60_000 ms.
    /// Defaults to <c>false</c> so accidentally-stale-for-an-hour deployments are caught
    /// at reload validation. Set to <c>true</c> to explicitly allow long TTLs.
    /// </summary>
    public bool AllowLongTtl { get; init; } = false;

    /// <summary>
    /// LRU cap on the number of entries per-PLC. Past this cap, the next insert evicts
    /// the least-recently-used entry. Defaults to 1000 — comfortable for a 54-PLC fleet
    /// with short TTLs.
    /// </summary>
    public int MaxEntriesPerPlc { get; init; } = 1000;

    /// <summary>
    /// Background eviction loop tick in milliseconds. Each tick scans the cache and
    /// removes entries past their <c>ExpiresAtUtc</c>. Defaults to 5000 ms; values below
    /// 100 ms are clamped at 100 to avoid pathologically tight loops.
    /// </summary>
    public int EvictionIntervalMs { get; init; } = 5000;
}

/// <summary>
/// Schema-level validation for <see cref="MbproxyOptions"/>. Business-rule validation
/// (duplicate addresses, port conflicts) is delegated to <see cref="Configuration.ReloadValidator"/>.
/// </summary>
public sealed class MbproxyOptionsValidator : IValidateOptions<MbproxyOptions>
{
    public ValidateOptionsResult Validate(string? name, MbproxyOptions options)
    {
        var errors = new List<string>();
        bool allowLongTtl = options.Cache.AllowLongTtl;

        foreach (var tag in options.BcdTags.Global)
        {
            if (tag.Width != 16 && tag.Width != 32)
                errors.Add($"BcdTags.Global: Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32.");
            ValidateCacheTtl(errors, $"BcdTags.Global Address {tag.Address}", tag.CacheTtlMs, allowLongTtl);
        }

        for (int i = 0; i < options.Plcs.Count; i++)
        {
            var plc = options.Plcs[i];

            // Per-PLC default TTL bounds.
            if (plc.DefaultCacheTtlMs < 0)
                errors.Add($"Plcs[{i}] ({plc.Name}): DefaultCacheTtlMs must be >= 0.");
            else if (plc.DefaultCacheTtlMs > 60_000 && !allowLongTtl)
                errors.Add(
                    $"Plcs[{i}] ({plc.Name}): DefaultCacheTtlMs={plc.DefaultCacheTtlMs} exceeds the 60_000 ms safety cap; " +
                    $"set Cache.AllowLongTtl=true to opt in.");

            if (plc.BcdTags is { } overrides)
            {
                foreach (var tag in overrides.Add)
                {
                    if (tag.Width != 16 && tag.Width != 32)
                        errors.Add($"Plcs[{i}] ({plc.Name}): BcdTags.Add Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32.");
                    ValidateCacheTtl(errors, $"Plcs[{i}] ({plc.Name}) BcdTags.Add Address {tag.Address}",
                        tag.CacheTtlMs, allowLongTtl);
                }
            }
        }

        // Cache section ranges.
        if (options.Cache.MaxEntriesPerPlc < 0)
            errors.Add($"Cache.MaxEntriesPerPlc must be >= 0; got {options.Cache.MaxEntriesPerPlc}.");
        if (options.Cache.EvictionIntervalMs < 0)
            errors.Add($"Cache.EvictionIntervalMs must be >= 0; got {options.Cache.EvictionIntervalMs}.");

        // Connection timeouts must be strictly positive. A 0 or negative value produces
        // a CancelAfter(0) that fires immediately and breaks every backend connect/request.
        if (options.Connection.BackendConnectTimeoutMs <= 0)
            errors.Add(
                $"Connection.BackendConnectTimeoutMs must be > 0; got {options.Connection.BackendConnectTimeoutMs}.");
        if (options.Connection.BackendRequestTimeoutMs <= 0)
            errors.Add(
                $"Connection.BackendRequestTimeoutMs must be > 0; got {options.Connection.BackendRequestTimeoutMs}.");
        if (options.Connection.GracefulShutdownTimeoutMs <= 0)
            errors.Add(
                $"Connection.GracefulShutdownTimeoutMs must be > 0; got {options.Connection.GracefulShutdownTimeoutMs}.");

        if (options.AdminPushIntervalMs <= 0)
            errors.Add($"AdminPushIntervalMs must be > 0; got {options.AdminPushIntervalMs}.");

        // Keepalive section ranges. Cross-field rules (heartbeat interval vs request
        // timeout) are enforced in ReloadValidator.
        var ka = options.Connection.Keepalive;
        if (ka.TcpIdleTimeMs <= 0)
            errors.Add($"Connection.Keepalive.TcpIdleTimeMs must be > 0; got {ka.TcpIdleTimeMs}.");
        if (ka.TcpProbeIntervalMs <= 0)
            errors.Add($"Connection.Keepalive.TcpProbeIntervalMs must be > 0; got {ka.TcpProbeIntervalMs}.");
        if (ka.TcpProbeCount <= 0)
            errors.Add($"Connection.Keepalive.TcpProbeCount must be > 0; got {ka.TcpProbeCount}.");
        if (ka.BackendHeartbeatIdleMs <= 0)
            errors.Add($"Connection.Keepalive.BackendHeartbeatIdleMs must be > 0; got {ka.BackendHeartbeatIdleMs}.");
        if (ka.BackendHeartbeatProbeAddress is < 0 or > 65535)
            errors.Add(
                $"Connection.Keepalive.BackendHeartbeatProbeAddress must be in [0, 65535]; " +
                $"got {ka.BackendHeartbeatProbeAddress}.");

        return errors.Count > 0
            ? ValidateOptionsResult.Fail(errors)
            : ValidateOptionsResult.Success;
    }

    private static void ValidateCacheTtl(List<string> errors, string context, int? ttlMs, bool allowLongTtl)
    {
        if (ttlMs is null) return;
        int value = ttlMs.Value;
        if (value < 0)
            errors.Add($"{context}: CacheTtlMs must be >= 0; got {value}.");
        else if (value > 60_000 && !allowLongTtl)
            errors.Add(
                $"{context}: CacheTtlMs={value} exceeds the 60_000 ms safety cap; " +
                $"set Cache.AllowLongTtl=true to opt in.");
    }
}
