using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;

namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;

/// <summary>
///     Static factory registration helper for <see cref="MTConnectDriver"/>. The Host registers it
///     once at startup; the bootstrapper then materialises MTConnect <c>DriverInstance</c> rows from
///     the deployed configuration into live driver instances. Mirrors
///     <c>ModbusDriverFactoryExtensions</c> / <c>FocasDriverFactoryExtensions</c>.
/// </summary>
/// <remarks>
///     <para>
///         <b>There is exactly one parser for the config document.</b> This factory does not carry a
///         config DTO of its own — it delegates to <see cref="MTConnectDriver.ParseOptions"/>, which
///         the driver itself must own because the runtime delivers a config <i>change</i> to a live
///         instance (<c>DriverInstanceActor</c> assigns its driver once and calls
///         <c>ReinitializeAsync(newJson)</c> thereafter). A second DTO here would drift from that
///         one silently, and the drift would first show up at deploy time as an operator's edit
///         being applied differently by the two paths — or not at all.
///     </para>
///     <para>
///         <b>Construction opens nothing.</b> The Wave-0 universal discovery browser builds a
///         throwaway instance through this factory purely to read
///         <c>ITagDiscovery.SupportsOnlineDiscovery</c>, and never initializes it — so a factory
///         that dialled the Agent (or that pre-built an <see cref="IMTConnectAgentClient"/>) would
///         open a connection per AdminUI browse render against a possibly-unreachable Agent. The
///         agent-client factory is therefore left null: <see cref="MTConnectDriver"/> builds the
///         production client inside <c>InitializeAsync</c>.
///     </para>
///     <para>
///         <b>The instance is returned as its concrete type.</b> The runtime resolves every optional
///         capability (<see cref="IReadable"/>, <see cref="ISubscribable"/>,
///         <see cref="ITagDiscovery"/>, <see cref="IHostConnectivityProbe"/>,
///         <see cref="IRediscoverable"/>) by pattern-matching the object
///         <see cref="DriverFactoryRegistry"/> hands back, so nothing here may narrow it — a
///         narrowed return is how a capability ends up implemented but never dispatched. There is
///         deliberately no <c>IWritable</c>: the MTConnect Agent surface is read-only.
///     </para>
/// </remarks>
public static class MTConnectDriverFactoryExtensions
{
    /// <summary>
    ///     The <c>DriverInstance.DriverType</c> value this factory answers to.
    /// </summary>
    /// <remarks>
    ///     <b>Aliased to <see cref="DriverTypeNames.MTConnect"/>, not a literal.</b> The registry key,
    ///     the instance's self-reported <see cref="MTConnectDriver.DriverType"/>, the probe's
    ///     <c>DriverType</c>, and the constant every dispatch map keys off are therefore the same
    ///     symbol — the repo-wide fix for the historical <c>TwinCat</c>/<c>Focas</c> drift, where a
    ///     driver's own spelling silently diverged from the constant and dispatch maps missed it.
    ///     Retained as a named const (rather than deleted in favour of the constant) so the existing
    ///     factory callers and the sibling <c>*DriverFactoryExtensions.DriverTypeName</c> convention
    ///     keep working; it is now an alias with no independent value.
    /// </remarks>
    public const string DriverTypeName = DriverTypeNames.MTConnect;

    /// <summary>
    ///     Register the MTConnect factory with the driver registry. The optional
    ///     <paramref name="loggerFactory"/> is captured at registration time and used to construct
    ///     an <see cref="ILogger{TCategoryName}"/> per driver instance — without it the driver runs
    ///     with the null logger (tests and standalone callers stay unchanged).
    /// </summary>
    /// <param name="registry">The driver factory registry to register with.</param>
    /// <param name="loggerFactory">Optional logger factory for creating loggers per driver instance.</param>
    public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
    {
        ArgumentNullException.ThrowIfNull(registry);
        registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
    }

    /// <summary>Public for the Server-side bootstrapper + test consumers.</summary>
    /// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
    /// <param name="driverConfigJson">The driver's <c>DriverConfig</c> JSON.</param>
    /// <returns>The constructed <see cref="MTConnectDriver"/> instance.</returns>
    public static MTConnectDriver CreateInstance(string driverInstanceId, string driverConfigJson)
        => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null);

    /// <summary>Logger-aware overload — used by <see cref="Register"/>'s closure when wired through DI.</summary>
    /// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
    /// <param name="driverConfigJson">The driver's <c>DriverConfig</c> JSON.</param>
    /// <param name="loggerFactory">Optional logger factory for creating loggers per driver instance.</param>
    /// <returns>The constructed <see cref="MTConnectDriver"/> instance.</returns>
    /// <exception cref="ArgumentException">
    ///     <paramref name="driverInstanceId"/> or <paramref name="driverConfigJson"/> is null or blank.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///     The config document is unparseable, deserialises to null, omits the required
    ///     <c>agentUri</c>, or carries an unauthorable enum/tag value. Throwing is correct at this
    ///     seam: a deployment carrying such a row must fail rather than register a driver pointed at
    ///     nothing, and the discovery browser's <c>CanBrowse</c> catches factory exceptions so a
    ///     half-authored config merely disables the address picker.
    /// </exception>
    public static MTConnectDriver CreateInstance(
        string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
        ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);

        // The single config authority — see the class remarks for why this is not a second DTO.
        // Note the asymmetry with the driver's own lifecycle path: an empty ("{}" / "[]") document
        // there means "no config supplied, keep the constructor options", but this factory HAS no
        // constructor options to keep, so an empty document is simply a config with no agentUri and
        // must fault.
        var options = MTConnectDriver.ParseOptions(driverInstanceId, driverConfigJson);

        // Named arguments deliberately: the ctor's trailing parameters are all optional, so a
        // future insertion could silently re-bind a positional argument to the wrong one.
        return new MTConnectDriver(
            options,
            driverInstanceId,
            agentClientFactory: null,
            logger: loggerFactory?.CreateLogger<MTConnectDriver>());
    }
}
