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;

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;

    /// <summary>Creates a new browser. Logger defaults to <see cref="NullLogger{T}"/>.</summary>
    /// <param name="logger">Optional logger; null is allowed for unit-test construction.</param>
    public GalaxyDriverBrowser(ILogger<GalaxyDriverBrowser>? logger = null)
    {
        _logger = logger ?? NullLogger<GalaxyDriverBrowser>.Instance;
    }

    /// <summary>Driver type key — matches the AdminUI's persisted "GalaxyMxGateway" value.</summary>
    // Hardcoded literal: this project references Driver.Galaxy.Contracts, not Driver.Galaxy,
    // so GalaxyDriverFactoryExtensions.DriverTypeName isn't available here.
    public string DriverType => "GalaxyMxGateway";

    /// <summary>
    ///     Deserializes a <see cref="GalaxyDriverOptions"/> blob, opens a transient
    ///     <see cref="GalaxyRepositoryClient"/> against the configured gateway endpoint,
    ///     and returns a browse session over it. The session owns the client and disposes
    ///     it on <see cref="IBrowseSession.DisposeAsync"/>.
    /// </summary>
    /// <param name="configJson">Driver options serialized as JSON; same shape the runtime
    /// driver would consume.</param>
    /// <param name="cancellationToken">Cancellation for the connect phase only.</param>
    /// <exception cref="InvalidOperationException">
    ///     Thrown when the JSON deserialises to null, when <c>Gateway.Endpoint</c> is empty,
    ///     or when <c>MxAccess.ClientName</c> is empty.
    /// </exception>
    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 = BuildClientOptions(opts.Gateway);

        // 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
    ///     inline (a slim version of <c>GalaxyDriver.ResolveApiKey</c>) because the
    ///     Browser project doesn't reference Driver.Galaxy.
    /// </summary>
    private MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new()
    {
        Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
        ApiKey = ResolveApiKey(gw.ApiKeySecretRef),
        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,
    };

    /// <summary>
    ///     Resolves <c>env:NAME</c>, <c>file:PATH</c>, and <c>dev:KEY</c> prefixes;
    ///     anything else is treated as a literal cleartext key with a startup warning.
    ///     Slim mirror of <c>GalaxyDriver.ResolveApiKey</c> — the runtime version lives
    ///     in a sibling project the Browser intentionally doesn't reference.
    /// </summary>
    /// <param name="secretRef">The secret reference string to resolve.</param>
    private string ResolveApiKey(string secretRef)
    {
        ArgumentException.ThrowIfNullOrEmpty(secretRef);

        if (secretRef.StartsWith("env:", StringComparison.OrdinalIgnoreCase))
        {
            var name = secretRef[4..];
            var value = Environment.GetEnvironmentVariable(name);
            return !string.IsNullOrEmpty(value)
                ? value
                : throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' resolves to env var '{name}', but it is unset.");
        }

        if (secretRef.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
        {
            var path = secretRef[5..];
            if (!File.Exists(path))
            {
                throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' points at '{path}', which doesn't exist.");
            }
            var contents = File.ReadAllText(path).Trim();
            return !string.IsNullOrEmpty(contents)
                ? contents
                : throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' file '{path}' is empty.");
        }

        if (secretRef.StartsWith("dev:", StringComparison.OrdinalIgnoreCase))
        {
            // Explicit dev opt-in — no warning, the operator deliberately chose a
            // cleartext literal (dev box, parity rig).
            return secretRef[4..];
        }

        // Back-compat literal arm. An unprefixed string is treated as the literal
        // API key — but emit a warning so an operator who accidentally committed a
        // cleartext key into DriverConfig sees it when they open the address picker.
        _logger.LogWarning(
            "Galaxy.Gateway.ApiKeySecretRef is being treated as a literal cleartext API key. " +
            "Prefer env:NAME, file:PATH, or the explicit dev:KEY prefix for dev rigs — " +
            "a literal key in DriverConfig JSON is stored in cleartext in the central config DB.");
        return secretRef;
    }
}
