using Microsoft.Extensions.Logging;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;

/// <summary>
///     <see cref="IHistorianProvisioning"/> backed by the HistorianGateway <c>EnsureTags</c> path.
///     Non-historizable driver types are skipped (never built into a definition); the historizable
///     ones are mapped via <see cref="HistorianTypeMapper"/> and batched into a single
///     <c>EnsureTags</c> call.
/// </summary>
/// <remarks>
///     <b>Non-blocking.</b> A historian that is unreachable or errors must never fail an address-space
///     apply, so the gateway call is wrapped in a catch-all: any exception counts the whole sent batch
///     as <see cref="HistorianProvisionResult.Failed"/> and returns. The method never throws and never
///     logs tag values, hostnames, or credentials.
/// </remarks>
public sealed class GatewayTagProvisioner : IHistorianProvisioning
{
    /// <summary>
    ///     Storage rate (ms) stamped on every provisioned analog tag. The gateway's <c>EnsureTags</c> path
    ///     maps this straight to the AVEVA SDK's <c>storageRateMs</c>, which requires a positive, quantized
    ///     value (1000/5000/10000/…); the proto's <c>uint32</c> default of 0 makes the SDK throw
    ///     <see cref="ArgumentOutOfRangeException"/> ("Storage rate must be &gt; 0 ms"), failing the whole
    ///     provision. 1000 ms (1 s) matches the gateway's own live provisioner default and suits
    ///     continuous-historization analog tags.
    /// </summary>
    private const uint DefaultStorageRateMs = 1000;

    private readonly IHistorianGatewayClient _client;
    private readonly ILogger<GatewayTagProvisioner> _logger;

    /// <summary>Creates the provisioner over a gateway client seam.</summary>
    /// <param name="client">The gateway client used for the <c>EnsureTags</c> path.</param>
    /// <param name="logger">Logger for skip/failure diagnostics (never logs tag values).</param>
    public GatewayTagProvisioner(IHistorianGatewayClient client, ILogger<GatewayTagProvisioner> logger)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <inheritdoc />
    public async Task<HistorianProvisionResult> EnsureTagsAsync(
        IReadOnlyList<HistorianTagProvisionRequest> requests, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(requests);

        var definitions = new List<HistorianTagDefinition>(requests.Count);
        var skipped = 0;

        foreach (var request in requests)
        {
            if (!HistorianTypeMapper.IsHistorizable(request.DataType))
            {
                skipped++;
                // Log only the (non-sensitive) data type — never the tag name.
                _logger.LogDebug(
                    "Skipping provisioning of a non-historizable tag of type {DataType}.", request.DataType);
                continue;
            }

            definitions.Add(new HistorianTagDefinition
            {
                TagName = request.TagName,
                DataType = HistorianTypeMapper.ToHistorianDataType(request.DataType),
                // Proto string fields are non-nullable — coalesce absent metadata to empty.
                EngineeringUnit = request.EngineeringUnit ?? string.Empty,
                Description = request.Description ?? string.Empty,
                // Required: a 0 storage rate makes the gateway's EnsureTags throw (see the constant).
                StorageRateMs = DefaultStorageRateMs,
            });
        }

        // Every request was non-historizable — nothing to send. Skip the empty gateway round-trip.
        if (definitions.Count == 0)
        {
            return new HistorianProvisionResult(requests.Count, Ensured: 0, Skipped: skipped, Failed: 0);
        }

        try
        {
            var results = await _client.EnsureTagsAsync(definitions, ct).ConfigureAwait(false);
            var ensured = results.Results.Count(r => r.Success);
            var failed = Math.Max(0, definitions.Count - ensured);
            return new HistorianProvisionResult(requests.Count, ensured, skipped, failed);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // A plain shutdown cancellation is not a provisioning fault worth a scary Warning. Count
            // the unsent batch as Failed (it didn't land) but log quietly and never throw, keeping
            // the non-blocking contract.
            _logger.LogDebug("Tag provisioning cancelled at shutdown; deferred.");
            return new HistorianProvisionResult(requests.Count, Ensured: 0, Skipped: skipped, Failed: definitions.Count);
        }
        catch (Exception exception)
        {
            // Non-blocking: a failed EnsureTags never fails the apply. Count the whole sent batch as
            // Failed and return; log only the failure category (no tag values).
            _logger.LogWarning(
                "EnsureTags failed for {Count} historian tag(s) ({Exception}); provisioning deferred.",
                definitions.Count, exception.GetType().Name);
            return new HistorianProvisionResult(requests.Count, Ensured: 0, Skipped: skipped, Failed: definitions.Count);
        }
    }
}
