using System.Collections.Generic;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;

/// <summary>
///     Binds the <c>ServerHistorian</c> configuration section that gates the server-side
///     HistoryRead backend. When <see cref="Enabled"/> is <c>true</c>, <c>AddServerHistorian</c>
///     registers a read-only <c>HistorianGateway</c>-backed <c>IHistorianDataSource</c> (supplied by
///     the Host) in place of the <c>NullHistorianDataSource</c> default; otherwise the Null default
///     survives and HistoryRead returns <c>GoodNoData</c>-empty.
///     <para>
///         The read client talks gRPC to the <c>ZB.MOM.WW.HistorianGateway</c> sidecar
///         (<c>historian_gateway.v1</c>) over <see cref="Endpoint"/>, authenticating with the
///         peppered-HMAC <see cref="ApiKey"/> (<c>histgw_&lt;id&gt;_&lt;secret&gt;</c>) in the
///         <c>Authorization: Bearer</c> header. This is the READ path only — there are no
///         DatabasePath / drain / capacity / retention knobs (those belong to the write-side
///         <c>AlarmHistorian</c> store-and-forward sink). The client's own <see cref="CallTimeout"/>
///         bounds each read; the node manager adds no extra timeout.
///     </para>
/// </summary>
public sealed class ServerHistorianOptions
{
    /// <summary>The configuration section name this options class binds.</summary>
    public const string SectionName = "ServerHistorian";

    /// <summary>
    ///     When <c>true</c>, the HistorianGateway read client is registered as the
    ///     <c>IHistorianDataSource</c>; when <c>false</c> (the default) the no-op
    ///     <c>NullHistorianDataSource</c> stays in place and HistoryRead returns empty.
    /// </summary>
    public bool Enabled { get; init; }

    /// <summary>
    ///     Absolute gateway endpoint URI the read client dials (e.g. <c>https://host:5222</c>).
    ///     The scheme selects the transport: <c>https://</c> = TLS, <c>http://</c> = h2c plaintext.
    ///     Required when <see cref="Enabled"/> is <c>true</c>.
    /// </summary>
    public string Endpoint { get; init; } = "";

    /// <summary>
    ///     The peppered-HMAC API key (<c>histgw_&lt;id&gt;_&lt;secret&gt;</c>) the gateway validates
    ///     in the <c>Authorization: Bearer</c> header. Supply via the environment variable
    ///     <c>ServerHistorian__ApiKey</c> — never commit it to config. The value may be a literal
    ///     key or a <c>${secret:otopcua/historian/api-key}</c> token, which the pre-host secrets
    ///     expander resolves fail-closed (throwing if the secret is absent) before this options
    ///     class binds. Required when <see cref="Enabled"/> is <c>true</c>.
    /// </summary>
    public string ApiKey { get; init; } = "";

    /// <summary>When <c>true</c> (the default), the client connects over TLS; must match the <see cref="Endpoint"/> scheme.</summary>
    public bool UseTls { get; init; } = true;

    /// <summary>When <c>true</c>, the client accepts a self-signed / untrusted server certificate (dev / on-prem only).</summary>
    public bool AllowUntrustedServerCertificate { get; init; }

    /// <summary>Path to a PEM CA certificate that pins the gateway's TLS trust chain. Null or empty uses the OS trust store.</summary>
    public string? CaCertificatePath { get; init; }

    /// <summary>Per-call deadline applied to each unary gateway read. Defaults to 30 seconds.</summary>
    public TimeSpan CallTimeout { get; init; } = TimeSpan.FromSeconds(30);

    /// <summary>
    ///     The upper bound on the bounded over-fetch the HistoryRead-Raw paging uses to page WITHIN an
    ///     oversized "tie cluster" (more raw samples sharing one SourceTimestamp than the client's per-page
    ///     cap). When a resume read stalls on such a cluster, the node manager over-fetches up to
    ///     <c>MaxTieClusterOverfetch + 1</c> ties at that single timestamp (a <c>start == end</c> read) and
    ///     pages through them; this bounds how large a single-timestamp burst the server will buffer in
    ///     memory before it gives up and surfaces <c>BadHistoryOperationUnsupported</c> for that node. The
    ///     default (65536) comfortably covers any realistic same-millisecond burst.
    /// </summary>
    public int MaxTieClusterOverfetch { get; init; } = 65536;

    /// <summary>
    ///     Maximum number of historized nodes served CONCURRENTLY within one HistoryRead batch (arch-review
    ///     03/S3). The SDK's <c>HistoryRead*</c> override surface is synchronous, so each arm block-bridges
    ///     once at its boundary; this bound turns the per-node fan-out inside that boundary from sequential
    ///     (worst case <c>N × CallTimeout</c>) into <c>⌈N / P⌉ × CallTimeout</c>. It also caps the gateway-load
    ///     multiplication a single client can command. Default 4.
    /// </summary>
    public int HistoryReadBatchParallelism { get; init; } = 4;

    /// <summary>
    ///     Process-wide limit on the number of HistoryRead batches served CONCURRENTLY (arch-review 03/S3).
    ///     A flood of history clients degrades to <c>BadTooManyOperations</c> rather than exhausting the SDK
    ///     request-thread pool. Together with <see cref="HistoryReadBatchParallelism"/> this caps total
    ///     in-flight gateway reads at <c>MaxConcurrentHistoryReadBatches × HistoryReadBatchParallelism</c>
    ///     (default 16 × 4 = 64). Default 16.
    /// </summary>
    public int MaxConcurrentHistoryReadBatches { get; init; } = 16;

    /// <summary>
    ///     Per-request wall-clock deadline for a HistoryRead batch (arch-review 03/S3). A single request can
    ///     no longer hold an SDK request thread longer than this regardless of node count; a node still
    ///     in flight at the deadline surfaces <c>BadTimeout</c>. Non-positive = unbounded (a
    ///     <see cref="Validate"/> warning). Default 60 seconds.
    /// </summary>
    public TimeSpan HistoryReadDeadline { get; init; } = TimeSpan.FromSeconds(60);

    /// <summary>Returns operator-facing misconfiguration warnings for an <c>Enabled</c> historian
    /// (empty when disabled or correctly configured). Pure — the registration logs each entry.
    /// <para>
    ///     These are <b>warnings only</b>. The Host layers a fail-fast startup validator
    ///     (<c>ServerHistorianOptionsValidator</c>, wired via <c>AddValidatedOptions</c>) on top of this
    ///     for the <b>provably-crashing</b> subset — an empty/malformed <c>Endpoint</c> that is consumed
    ///     by an enabled historian or an enabled <c>AlarmHistorian</c> — so that config fails host start
    ///     with a named error instead of a raw <c>UriFormatException</c> crash-loop (archreview 06/S-11).
    ///     The empty-<c>ApiKey</c> / non-positive-<c>MaxTieClusterOverfetch</c> entries stay warnings
    ///     because they degrade rather than crash.
    /// </para></summary>
    /// <returns>Zero or more human-readable warning messages (never carrying secret values).</returns>
    public IReadOnlyList<string> Validate()
    {
        var warnings = new List<string>();
        if (!Enabled) return warnings;
        if (string.IsNullOrWhiteSpace(Endpoint))
            warnings.Add("ServerHistorian:Endpoint is empty while the historian is enabled — the read client has no gateway address to dial.");
        // archreview 06/S-11 warning parity: a non-empty but unparseable / non-absolute-http(s) Endpoint
        // is just as fatal (it throws in the gateway client factory). Warn on it here so the Runtime-side
        // registration logs describe malformed endpoints too, consistent with the Host's fail-fast
        // ServerHistorianOptionsValidator (which turns this same condition into a startup failure).
        else if (!Uri.TryCreate(Endpoint, UriKind.Absolute, out var uri)
                 || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
            warnings.Add($"ServerHistorian:Endpoint ('{Endpoint}') is not an absolute http(s) URI while the historian is enabled — the read client cannot dial it (e.g. https://host:5222).");
        if (string.IsNullOrWhiteSpace(ApiKey))
            warnings.Add("ServerHistorian:ApiKey is empty while the historian is enabled — the gateway gRPC surface will reject unauthenticated calls.");
        // MaxTieClusterOverfetch is intentionally checked AFTER the Enabled early-return above:
        // the over-fetch code path only runs when a real IHistorianDataSource is wired in,
        // so a zero/negative value is harmless (and noise-free) when the historian is disabled.
        if (MaxTieClusterOverfetch <= 0)
            warnings.Add($"ServerHistorian:MaxTieClusterOverfetch is {MaxTieClusterOverfetch} — must be > 0; HistoryRead-Raw cannot page within an oversized tie cluster and will surface BadHistoryOperationUnsupported for those reads.");
        // S3 HistoryRead concurrency knobs — a non-positive value disables the corresponding bound, so warn
        // (these gate the per-node fan-out / batch limiter that only run when a real data source is wired).
        if (HistoryReadBatchParallelism <= 0)
            warnings.Add($"ServerHistorian:HistoryReadBatchParallelism is {HistoryReadBatchParallelism} — must be > 0; HistoryRead falls back to serving each node sequentially.");
        if (MaxConcurrentHistoryReadBatches <= 0)
            warnings.Add($"ServerHistorian:MaxConcurrentHistoryReadBatches is {MaxConcurrentHistoryReadBatches} — must be > 0; the process-wide HistoryRead batch limiter is disabled.");
        if (HistoryReadDeadline <= TimeSpan.Zero)
            warnings.Add($"ServerHistorian:HistoryReadDeadline is {HistoryReadDeadline} — must be > 0; HistoryRead requests run unbounded (no per-request deadline).");
        return warnings;
    }
}
