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

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

/// <summary>
///     Static factory registration helper for <see cref="CalculationDriver"/>. The Server's
///     <c>DriverFactoryBootstrap</c> calls <see cref="Register"/> once at startup; the bootstrapper then
///     materialises <c>Calculation</c> DriverInstance rows into live driver instances. Mirrors
///     <c>ModbusDriverFactoryExtensions</c>, with the addition of a <see cref="ScriptRootLogger"/> so a
///     script failure fans out onto the <c>script-logs</c> topic (via the root logger's
///     <c>ScriptLogTopicSink</c>) — the calc driver has no Akka access of its own.
/// </summary>
public static class CalculationDriverFactoryExtensions
{
    /// <summary>The <c>DriverInstance.DriverType</c> string this factory registers under.</summary>
    public const string DriverTypeName = "Calculation";

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true,
        Converters = { new JsonStringEnumConverter() },
    };

    /// <summary>
    ///     Register the Calculation factory with the driver registry. The optional
    ///     <paramref name="loggerFactory"/> + <paramref name="scriptRoot"/> are captured at registration
    ///     time; both may be null (the guard test invokes <c>Register(registry)</c> reflectively with the
    ///     trailing parameters defaulted), in which case the driver runs with the null logger + a no-op
    ///     script-logger.
    /// </summary>
    /// <param name="registry">The driver factory registry to register with.</param>
    /// <param name="loggerFactory">Optional logger factory for per-instance diagnostics loggers.</param>
    /// <param name="scriptRoot">Optional root script logger (fans failure entries onto the <c>script-logs</c> topic).</param>
    public static void Register(
        DriverFactoryRegistry registry,
        ILoggerFactory? loggerFactory = null,
        ScriptRootLogger? scriptRoot = null)
    {
        ArgumentNullException.ThrowIfNull(registry);
        registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, scriptRoot));
    }

    /// <summary>Public overload for the Server-side bootstrapper + test consumers.</summary>
    /// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
    /// <param name="driverConfigJson">The merged driver config JSON (carries the <c>RawTags</c> array).</param>
    /// <returns>The constructed <see cref="CalculationDriver"/>.</returns>
    public static CalculationDriver CreateInstance(string driverInstanceId, string driverConfigJson)
        => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null, scriptRoot: null);

    /// <summary>Logger/script-logger-aware overload — used by <see cref="Register"/>'s closure via DI.</summary>
    /// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
    /// <param name="driverConfigJson">The merged driver config JSON (carries the <c>RawTags</c> array).</param>
    /// <param name="loggerFactory">Optional logger factory for per-instance diagnostics loggers.</param>
    /// <param name="scriptRoot">Optional root script logger for failure fan-out.</param>
    /// <returns>The constructed <see cref="CalculationDriver"/>.</returns>
    public static CalculationDriver CreateInstance(
        string driverInstanceId, string driverConfigJson,
        ILoggerFactory? loggerFactory, ScriptRootLogger? scriptRoot)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
        ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);

        var dto = JsonSerializer.Deserialize<CalculationDriverConfigDto>(driverConfigJson, JsonOptions)
            ?? throw new InvalidOperationException(
                $"Calculation driver config for '{driverInstanceId}' deserialised to null");

        var options = new CalculationDriverOptions
        {
            RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
            RunTimeout = dto.RunTimeoutMs is { } ms && ms > 0
                ? TimeSpan.FromMilliseconds(ms) : TimeSpan.FromSeconds(2),
        };

        return new CalculationDriver(
            options, driverInstanceId, scriptRoot,
            logger: loggerFactory?.CreateLogger<CalculationDriver>());
    }

    /// <summary>The merged-config DTO the calc factory binds from. The Calculation driver has no
    /// backend/endpoint config — only the injected raw tags (+ an optional evaluation-timeout override).</summary>
    internal sealed class CalculationDriverConfigDto
    {
        /// <summary>The driver's authored raw calc tags (RawPath + TagConfig blob + WriteIdempotent + DeviceName).</summary>
        public List<RawTagEntry>? RawTags { get; init; }

        /// <summary>Optional per-script evaluation wall-clock budget override, in milliseconds (default 2000).</summary>
        public int? RunTimeoutMs { get; init; }
    }
}
