using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser;

/// <summary>
///     Opens transient gateway connections for the AdminUI address picker. Mirrors the
///     runtime <c>GalaxyDriver.BuildClientOptions</c> pattern so the gateway sees the
///     same option shape, but tweaks <see cref="MxGatewayClientOptions.ApiKey"/>
///     resolution + the gateway-side client identity so browse sessions are
///     distinguishable from the runtime driver's live MX session.
/// </summary>
public sealed class GalaxyDriverBrowser : IDriverBrowser
{
    /// <summary>
    ///     Identifier used in gateway-side logs / metrics for AdminUI browse sessions.
    ///     Distinct from any runtime driver's <c>MxAccess.ClientName</c> so an operator
    ///     can tell the two apart when triaging.
    /// </summary>
    internal const string BrowseClientIdentity = "OtOpcUa-AdminUI-Browse";

    /// <summary>Hard cap on the time we'll wait for the initial gateway handshake.</summary>
    private static readonly TimeSpan ConnectBudget = TimeSpan.FromSeconds(30);

    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        PropertyNameCaseInsensitive = true,
    };

    private readonly ILogger<GalaxyDriverBrowser> _logger;
    private readonly ISecretResolver _secretResolver;

    /// <summary>Creates a new browser. Logger defaults to <see cref="NullLogger{T}"/>.</summary>
    /// <param name="secretResolver">Resolves the <c>secret:</c> arm of <c>Gateway.ApiKeySecretRef</c> (DI-injected).</param>
    /// <param name="logger">Optional logger; null is allowed for unit-test construction.</param>
    public GalaxyDriverBrowser(ISecretResolver secretResolver, ILogger<GalaxyDriverBrowser>? logger = null)
    {
        _secretResolver = secretResolver ?? throw new ArgumentNullException(nameof(secretResolver));
        _logger = logger ?? NullLogger<GalaxyDriverBrowser>.Instance;
    }

    /// <inheritdoc />
    // Hardcoded literal: this project references Driver.Galaxy.Contracts, not Driver.Galaxy,
    // so GalaxyDriverFactoryExtensions.DriverTypeName isn't available here.
    public string DriverType => "GalaxyMxGateway";

    // Deserializes a GalaxyDriverOptions blob, opens a transient GalaxyRepositoryClient
    // against the configured gateway endpoint, and returns a browse session over it. The
    // session owns the client and disposes it on IBrowseSession.DisposeAsync. Throws
    // InvalidOperationException when the JSON deserialises to null, when Gateway.Endpoint
    // is empty, or when MxAccess.ClientName is empty.
    /// <inheritdoc />
    public async Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken)
    {
        var opts = JsonSerializer.Deserialize<GalaxyDriverOptions>(configJson, JsonOpts)
            ?? throw new InvalidOperationException("Galaxy options deserialized to null.");

        if (string.IsNullOrWhiteSpace(opts.Gateway.Endpoint))
            throw new InvalidOperationException("Galaxy browser requires Gateway.Endpoint.");

        // The form persists MXAccess identity as ClientName (there is no separate
        // "galaxy name" knob on the driver — the gateway picks the galaxy via its
        // own GalaxyRepository config). Refuse a blank ClientName so the gateway side
        // doesn't see anonymous browse sessions during triage.
        if (string.IsNullOrWhiteSpace(opts.MxAccess.ClientName))
            throw new InvalidOperationException("Galaxy browser requires MxAccess.ClientName.");

        var clientOpts = await BuildClientOptionsAsync(opts.Gateway, cancellationToken).ConfigureAwait(false);

        // 30s wall-clock budget for the connect phase, linked to the caller's token so
        // an AdminUI cancel still wins early.
        using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        connectCts.CancelAfter(ConnectBudget);

        GalaxyRepositoryClient? client = null;
        try
        {
            client = GalaxyRepositoryClient.Create(clientOpts);

            // TestConnectionAsync gives the gateway a chance to surface auth / TLS / DNS
            // failures synchronously inside the connect budget rather than waiting for
            // the first DiscoverHierarchyAsync call to fail. The client's own
            // ConnectTimeout already bounds the underlying gRPC handshake; the linked
            // CTS layered on top guarantees the AdminUI never blocks past 30s.
            await client.TestConnectionAsync(connectCts.Token).ConfigureAwait(false);

            _logger.LogInformation(
                "AdminUI Galaxy browse session opened against {Endpoint} (admin-client={Identity}, runtime-client={RuntimeClient})",
                opts.Gateway.Endpoint, BrowseClientIdentity, opts.MxAccess.ClientName);

            var session = new GalaxyBrowseSession(client);
            client = null; // Ownership transferred — keep finally from disposing.
            return session;
        }
        catch
        {
            if (client is not null)
            {
                try
                {
                    await client.DisposeAsync().ConfigureAwait(false);
                }
                catch
                {
                    // Best-effort cleanup; the original exception is more useful.
                }
            }
            throw;
        }
    }

    /// <summary>
    ///     Build the gateway client options from the form's Gateway section. Mirrors the
    ///     runtime driver's <c>GalaxyDriver.BuildClientOptions</c> field-for-field so the
    ///     gateway sees an identical option shape. The API-key reference is resolved via
    ///     the shared <see cref="GalaxySecretRef.ResolveApiKeyAsync"/> in Driver.Galaxy.Contracts
    ///     (the same resolver the runtime driver uses), so browse and runtime stay in lock-step.
    ///     The <c>secret:</c> arm is async, so the key is resolved into a local before the
    ///     object initializer (you can't await inside one).
    /// </summary>
    private async Task<MxGatewayClientOptions> BuildClientOptionsAsync(
        GalaxyGatewayOptions gw, CancellationToken cancellationToken)
    {
        var apiKey = await GalaxySecretRef
            .ResolveApiKeyAsync(gw.ApiKeySecretRef, _secretResolver, _logger, cancellationToken)
            .ConfigureAwait(false);
        return new MxGatewayClientOptions
        {
            Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
            ApiKey = apiKey,
            UseTls = gw.UseTls,
            CaCertificatePath = gw.CaCertificatePath,
            ConnectTimeout = TimeSpan.FromSeconds(gw.ConnectTimeoutSeconds),
            DefaultCallTimeout = TimeSpan.FromSeconds(gw.DefaultCallTimeoutSeconds),
            StreamTimeout = gw.StreamTimeoutSeconds > 0
                ? TimeSpan.FromSeconds(gw.StreamTimeoutSeconds)
                : null,
        };
    }
}
