using Mbproxy.Bcd;
using Mbproxy.Options;

namespace Mbproxy.Configuration;

/// <summary>
/// Validates an incoming <see cref="MbproxyOptions"/> snapshot before any state mutation
/// is attempted. All cross-PLC checks (uniqueness, port collisions) live here.
/// Per-PLC tag-list well-formedness is delegated to <see cref="BcdTagMapBuilder.Build"/>.
///
/// <para>Usage:</para>
/// <code>
/// if (!ReloadValidator.Validate(next, out var errors))
///     // log errors and abort reload
/// </code>
/// </summary>
internal static class ReloadValidator
{
    /// <summary>
    /// Validates <paramref name="next"/>. Returns <c>true</c> when valid.
    ///
    /// <para>Checks performed (in order):</para>
    /// <list type="number">
    ///   <item>All PLC names are non-empty and unique (ordinal comparison).</item>
    ///   <item>All <c>ListenPort</c> values are in [1, 65535] and unique.</item>
    ///   <item><c>AdminPort</c> is in [1, 65535] and does not collide with any <c>ListenPort</c>.</item>
    ///   <item>For each PLC, <see cref="BcdTagMapBuilder.Build"/> reports no errors.</item>
    /// </list>
    /// </summary>
    public static bool Validate(MbproxyOptions next, out IReadOnlyList<string> errors)
    {
        var errs = new List<string>();

        // ── 1. PLC name uniqueness ────────────────────────────────────────────
        var seenNames = new HashSet<string>(StringComparer.Ordinal);
        for (int i = 0; i < next.Plcs.Count; i++)
        {
            var plc = next.Plcs[i];
            if (string.IsNullOrWhiteSpace(plc.Name))
            {
                errs.Add($"Plcs[{i}]: Name must be non-empty.");
            }
            else if (!seenNames.Add(plc.Name))
            {
                errs.Add($"Plcs[{i}]: Duplicate PLC name '{plc.Name}'.");
            }
        }

        // ── 2. ListenPort uniqueness and range ────────────────────────────────
        var seenPorts = new Dictionary<int, string>(next.Plcs.Count);  // port → PLC name
        foreach (var plc in next.Plcs)
        {
            if (plc.ListenPort is < 1 or > 65535)
            {
                errs.Add($"Plc '{plc.Name}': ListenPort {plc.ListenPort} is out of range [1, 65535].");
            }
            else if (!seenPorts.TryAdd(plc.ListenPort, plc.Name))
            {
                errs.Add($"Plc '{plc.Name}': Duplicate ListenPort {plc.ListenPort} " +
                         $"(already used by '{seenPorts[plc.ListenPort]}').");
            }
        }

        // ── 3. AdminPort range and collision ─────────────────────────────────
        int adminPort = next.AdminPort;
        if (adminPort is < 1 or > 65535)
        {
            errs.Add($"AdminPort {adminPort} is out of range [1, 65535].");
        }
        else if (seenPorts.TryGetValue(adminPort, out string? clashPlc))
        {
            errs.Add($"AdminPort {adminPort} collides with ListenPort of PLC '{clashPlc}'.");
        }

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

        // ── 4. Per-PLC tag-map build ──────────────────────────────────────────
        // BcdTagMapBuilder.Build is the single source of truth for tag-list
        // well-formedness; we must not duplicate its validation logic here.
        // Also re-check the RESOLVED per-tag CacheTtlMs against AllowLongTtl. The raw-
        // input check at section 5 covers explicit per-tag and per-PLC-default values,
        // but defensively re-validating the post-fold values catches any future fold
        // logic that produces a value above the gate.
        bool allowLongTtlForResolved = next.Cache.AllowLongTtl;
        foreach (var plc in next.Plcs)
        {
            var result = BcdTagMapBuilder.Build(next.BcdTags, plc.BcdTags, plc.DefaultCacheTtlMs);
            foreach (var err in result.Errors)
                errs.Add($"Plc '{plc.Name}': BCD tag map error ({err.Kind}): {err.Message}");

            if (!allowLongTtlForResolved)
            {
                foreach (var tag in result.Map.All)
                {
                    if (tag.CacheTtlMs > 60_000)
                        errs.Add(
                            $"Plc '{plc.Name}': resolved CacheTtlMs for Address {tag.Address} is " +
                            $"{tag.CacheTtlMs} ms (exceeds 60_000) without Cache.AllowLongTtl=true.");
                }
            }
        }

        // ── 5. Cache TTL bounds ───────────────────────────────────────────────
        // The MbproxyOptionsValidator catches these at schema time too, but ReloadValidator
        // is the gate that the hot-reload path consults directly so re-checking here keeps
        // both paths internally consistent (and the validator runs against tag-map-resolved
        // BcdTag.CacheTtlMs values too).
        bool allowLongTtl = next.Cache.AllowLongTtl;
        foreach (var tag in next.BcdTags.Global)
        {
            CheckTtl(errs, $"BcdTags.Global Address {tag.Address}", tag.CacheTtlMs, allowLongTtl);
        }
        foreach (var plc in next.Plcs)
        {
            if (plc.DefaultCacheTtlMs > 60_000 && !allowLongTtl)
                errs.Add(
                    $"Plc '{plc.Name}': DefaultCacheTtlMs={plc.DefaultCacheTtlMs} exceeds 60_000 ms " +
                    "without Cache.AllowLongTtl=true.");
            else if (plc.DefaultCacheTtlMs < 0)
                errs.Add($"Plc '{plc.Name}': DefaultCacheTtlMs must be >= 0; got {plc.DefaultCacheTtlMs}.");

            if (plc.BcdTags?.Add is { } addList)
            {
                foreach (var tag in addList)
                    CheckTtl(errs, $"Plc '{plc.Name}' BcdTags.Add Address {tag.Address}",
                        tag.CacheTtlMs, allowLongTtl);
            }
        }
        if (next.Cache.MaxEntriesPerPlc < 0)
            errs.Add($"Cache.MaxEntriesPerPlc must be >= 0; got {next.Cache.MaxEntriesPerPlc}.");
        if (next.Cache.EvictionIntervalMs < 0)
            errs.Add($"Cache.EvictionIntervalMs must be >= 0; got {next.Cache.EvictionIntervalMs}.");

        // Connection timeouts must be > 0. A reload that sets any of these to 0 or
        // negative would break the runtime; reject the reload as a whole.
        if (next.Connection.BackendConnectTimeoutMs <= 0)
            errs.Add(
                $"Connection.BackendConnectTimeoutMs must be > 0; got {next.Connection.BackendConnectTimeoutMs}.");
        if (next.Connection.BackendRequestTimeoutMs <= 0)
            errs.Add(
                $"Connection.BackendRequestTimeoutMs must be > 0; got {next.Connection.BackendRequestTimeoutMs}.");
        if (next.Connection.GracefulShutdownTimeoutMs <= 0)
            errs.Add(
                $"Connection.GracefulShutdownTimeoutMs must be > 0; got {next.Connection.GracefulShutdownTimeoutMs}.");

        // ── 6. Keepalive section ──────────────────────────────────────────────
        // Schema bounds are also checked in MbproxyOptionsValidator; re-checking here keeps
        // the hot-reload gate self-contained. The cross-field rule (heartbeat interval must
        // sit above the request timeout, or it would fire continuously) lives only here.
        var ka = next.Connection.Keepalive;
        if (ka.TcpIdleTimeMs <= 0)
            errs.Add($"Connection.Keepalive.TcpIdleTimeMs must be > 0; got {ka.TcpIdleTimeMs}.");
        if (ka.TcpProbeIntervalMs <= 0)
            errs.Add($"Connection.Keepalive.TcpProbeIntervalMs must be > 0; got {ka.TcpProbeIntervalMs}.");
        if (ka.TcpProbeCount <= 0)
            errs.Add($"Connection.Keepalive.TcpProbeCount must be > 0; got {ka.TcpProbeCount}.");
        if (ka.BackendHeartbeatProbeAddress is < 0 or > 65535)
            errs.Add(
                $"Connection.Keepalive.BackendHeartbeatProbeAddress must be in [0, 65535]; " +
                $"got {ka.BackendHeartbeatProbeAddress}.");
        if (ka.BackendHeartbeatIdleMs <= next.Connection.BackendRequestTimeoutMs)
            errs.Add(
                $"Connection.Keepalive.BackendHeartbeatIdleMs ({ka.BackendHeartbeatIdleMs}) must be greater " +
                $"than Connection.BackendRequestTimeoutMs ({next.Connection.BackendRequestTimeoutMs}); " +
                "a heartbeat interval at or below the request timeout would fire continuously.");

        errors = errs;
        return errs.Count == 0;
    }

    private static void CheckTtl(List<string> errs, string context, int? ttl, bool allowLongTtl)
    {
        if (ttl is null) return;
        int v = ttl.Value;
        if (v < 0)
            errs.Add($"{context}: CacheTtlMs must be >= 0; got {v}.");
        else if (v > 60_000 && !allowLongTtl)
            errs.Add(
                $"{context}: CacheTtlMs={v} exceeds 60_000 ms without Cache.AllowLongTtl=true.");
    }
}
