using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.Secrets.Abstractions;

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

/// <summary>
///     Registers the OPC UA Client driver with the <see cref="DriverFactoryRegistry"/>. The driver's
///     <c>DriverConfig</c> JSON deserialises directly into <see cref="OpcUaClientDriverOptions"/>
///     (the same shape <see cref="OpcUaClientDriverProbe"/> reads), so no separate DTO is needed —
///     unlike the protocol drivers, this driver has no pre-declared "Tags" DTO path to retire.
///     v3: the deploy artifact's authored raw tags bind straight onto
///     <see cref="OpcUaClientDriverOptions.RawTags"/> (a <c>RawTagEntry</c> list) through the same
///     deserialiser; the <c>EndpointUrl</c> binding is unchanged (endpoint→DeviceConfig is Wave C).
///     Mirrors <c>ModbusDriverFactoryExtensions</c> / <c>GalaxyDriverFactoryExtensions</c>.
/// </summary>
public static class OpcUaClientDriverFactoryExtensions
{
    /// <summary>Driver type name — matches <c>DriverInstance.DriverType</c> values.</summary>
    public const string DriverTypeName = "OpcUaClient";

    // Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
    // The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
    // AuthType / TargetNamespaceKind) be authored as their string names — the natural form
    // for human-edited + AdminUI-emitted DriverConfig JSON.
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

    /// <summary>Register the OpcUaClient factory with the driver registry, threading the shared secret resolver.</summary>
    /// <param name="registry">The driver factory registry to register with.</param>
    /// <param name="loggerFactory">Optional logger factory used to create per-instance loggers.</param>
    /// <param name="secretResolver">
    ///     The shared secret resolver (from DI) used to resolve the <c>secret:</c> arm of each
    ///     instance's <c>Password</c> / <c>UserCertificatePassword</c> at session-open. Defaults to
    ///     the <see cref="NullSecretResolver"/> null-object for the test/standalone path — callers
    ///     that need a working <c>secret:</c> credential ref must thread the DI resolver.
    /// </param>
    public static void Register(
        DriverFactoryRegistry registry,
        ILoggerFactory? loggerFactory = null,
        ISecretResolver? secretResolver = null)
    {
        ArgumentNullException.ThrowIfNull(registry);
        var resolver = secretResolver ?? NullSecretResolver.Instance;
        registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, resolver));
    }

    /// <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 JSON configuration string for the driver.</param>
    /// <param name="loggerFactory">Optional logger factory for the per-instance logger.</param>
    /// <param name="secretResolver">
    ///     Optional shared secret resolver for the driver's <c>secret:</c> credential arm; defaults
    ///     to the <see cref="NullSecretResolver"/> null-object (fail-closed on a <c>secret:</c> ref).
    /// </param>
    /// <returns>A configured <see cref="OpcUaClientDriver"/>.</returns>
    public static OpcUaClientDriver CreateInstance(
        string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null,
        ISecretResolver? secretResolver = null)
    {
        return new OpcUaClientDriver(
            ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId,
            loggerFactory?.CreateLogger<OpcUaClientDriver>(),
            secretResolver ?? NullSecretResolver.Instance);
    }

    /// <summary>
    ///     Parses an OpcUaClient <c>DriverConfig</c> JSON document into typed options. Extracted so
    ///     <see cref="OpcUaClientDriver.InitializeAsync"/> can re-parse a CHANGED config on reinitialize
    ///     (Gitea #516) instead of serving the options it was constructed with. Note the driver's own
    ///     doc-comment previously claimed "resolving on every InitializeAsync picks up rotations" — that was
    ///     true of SECRET rotation only; the endpoint, security policy and tag set were all frozen at
    ///     construction.
    /// </summary>
    /// <param name="driverInstanceId">The unique driver instance identifier.</param>
    /// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
    /// <returns>The parsed <see cref="OpcUaClientDriverOptions"/>.</returns>
    internal static OpcUaClientDriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
        ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);

        return JsonSerializer.Deserialize<OpcUaClientDriverOptions>(driverConfigJson, JsonOptions)
            ?? throw new InvalidOperationException(
                $"OpcUaClient driver config for '{driverInstanceId}' deserialised to null");
    }
}
