using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;

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

/// <summary>
///     Driver-side wrapper around the gateway's <see cref="MxGatewaySession"/>. Owns the
///     MXAccess <c>Register</c> handle, caches the per-tag item handles AddItem returns,
///     and coordinates the read / write / subscribe call paths. PRs 4.2-4.5 fill this in
///     incrementally:
///     <list type="bullet">
///       <item>PR 4.2 (this PR) — skeleton + lifecycle wiring.</item>
///       <item>PR 4.3 — write path.</item>
///       <item>PR 4.4 — subscription registry + event pump + the production
///         <see cref="IGalaxyDataReader"/> implementation that drives the read path.</item>
///       <item>PR 4.5 — reconnect supervisor.</item>
///     </list>
/// </summary>
public sealed class GalaxyMxSession : IAsyncDisposable
{
    private readonly GalaxyMxAccessOptions _options;
    private readonly ILogger _logger;

    // Owned gateway client + session — populated when ConnectAsync runs. Tests can leave
    // them null and exercise the surface via injected IGalaxyDataReader fakes.
    private MxGatewayClient? _ownedClient;
    private MxGatewaySession? _session;
    private int _serverHandle;
    private bool _connected;
    private bool _disposed;

    /// <summary>Initializes a new instance of the GalaxyMxSession class.</summary>
    /// <param name="options">MX Access configuration options.</param>
    /// <param name="logger">Optional logger instance; uses NullLogger if not provided.</param>
    public GalaxyMxSession(GalaxyMxAccessOptions options, ILogger? logger = null)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _logger = logger ?? NullLogger.Instance;
    }

    /// <summary>
    ///     Gets a value indicating whether a session is currently established. Set <c>true</c> on a
    ///     successful connect/attach and <c>false</c> after teardown/dispose — tracks the same
    ///     <c>_connected</c> flag that <see cref="ConnectAsync"/> guards on, so the two never disagree.
    /// </summary>
    public bool IsConnected => _connected;

    /// <summary>
    ///     Server-side handle returned by MXAccess <c>Register</c>. Zero before
    ///     <see cref="ConnectAsync"/> opens the session.
    /// </summary>
    public int ServerHandle => _serverHandle;

    /// <summary>
    ///     Test seam — when set, replaces the real "open session + register" work so unit tests can
    ///     exercise the connect / recreate orchestration without a live gateway (the SDK session/client
    ///     types are sealed + internal-ctor and cannot be faked). Null in production = real gateway path.
    /// </summary>
    internal Func<CancellationToken, Task>? OpenAndRegisterOverrideForTests { get; set; }

    /// <summary>
    ///     Connect the underlying gateway client + open an MXAccess session + register the
    ///     configured client name. Idempotent — second calls are no-ops while a connect has
    ///     already succeeded. Use <see cref="RecreateAsync"/> to force a rebuild after the
    ///     gateway restarts and leaves a faulted / stale session handle.
    /// </summary>
    /// <param name="clientOptions">The MX gateway client options.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task ConnectAsync(MxGatewayClientOptions clientOptions, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (_connected) return;

        try
        {
            if (OpenAndRegisterOverrideForTests is not null)
            {
                await OpenAndRegisterOverrideForTests(cancellationToken).ConfigureAwait(false);
            }
            else
            {
                _ownedClient = MxGatewayClient.Create(clientOptions);
                _session = await _ownedClient.OpenSessionAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
                _serverHandle = await _session.RegisterAsync(_options.ClientName, cancellationToken).ConfigureAwait(false);
            }

            _connected = true;
            _logger.LogInformation(
                "GalaxyMxSession connected — clientName={ClientName} serverHandle={Handle}",
                _options.ClientName, _serverHandle);
        }
        catch
        {
            // A partial open (e.g. OpenSession succeeded but Register threw) must not leave a half-open
            // handle that blocks the next reconnect. Tear the partial state down so a retry rebuilds cleanly.
            await TeardownSessionAsync().ConfigureAwait(false);
            throw;
        }
    }

    /// <summary>
    ///     Disposes the current (faulted / stale) session + owned client and rebuilds. Use this on
    ///     reconnect: <see cref="ConnectAsync"/> alone no-ops while a (possibly dead) session handle is
    ///     still present, so the supervisor's reopen must route through here to actually re-establish.
    /// </summary>
    /// <param name="clientOptions">The MX gateway client options.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task RecreateAsync(MxGatewayClientOptions clientOptions, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        await TeardownSessionAsync().ConfigureAwait(false);
        await ConnectAsync(clientOptions, cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    ///     Test seam — attach a session opened externally (e.g. against an in-process gw
    ///     fake). Skips the gateway-client construction so tests can drive the session
    ///     surface without spinning a real gRPC channel. Caller retains client ownership.
    /// </summary>
    /// <param name="session">The MX gateway session to attach.</param>
    /// <param name="serverHandle">The server handle value to use.</param>
    internal void AttachForTests(MxGatewaySession session, int serverHandle)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _serverHandle = serverHandle;
        _connected = true; // an attached session means connected — keeps IsConnected + the ConnectAsync no-op guard consistent.
    }

    /// <summary>
    ///     Returns the underlying gateway session. Null until <see cref="ConnectAsync"/> or
    ///     <see cref="AttachForTests"/> runs. PR 4.3 / 4.4 use this to issue commands.
    /// </summary>
    public MxGatewaySession? Session => _session;

    /// <summary>Disposes the session and underlying gateway client resources.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;
        await TeardownSessionAsync().ConfigureAwait(false);
    }

    /// <summary>
    ///     Resets the connect flag and best-effort disposes + nulls the owned session and client.
    ///     Shared by <see cref="DisposeAsync"/> (final teardown) and <see cref="RecreateAsync"/> /
    ///     the partial-open recovery path (rebuild teardown) — it does NOT set <c>_disposed</c>, so
    ///     a torn-down session can be re-opened.
    /// </summary>
    private async Task TeardownSessionAsync()
    {
        _connected = false;

        if (_session is not null)
        {
            try { await _session.DisposeAsync().ConfigureAwait(false); }
            catch (Exception ex) { _logger.LogWarning(ex, "GalaxyMxSession session dispose failed (best-effort)"); }
            _session = null;
        }

        if (_ownedClient is not null)
        {
            try { await _ownedClient.DisposeAsync().ConfigureAwait(false); }
            catch (Exception ex) { _logger.LogWarning(ex, "GalaxyMxSession client dispose failed (best-effort)"); }
            _ownedClient = null;
        }
    }
}
