using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc;

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

/// <summary>
///     Static factory registration helper for <see cref="FocasDriver"/>. Server's Program.cs
///     calls <see cref="Register"/> once at startup; the bootstrapper (task #248) then
///     materialises FOCAS DriverInstance rows from the central config DB into live driver
///     instances. Mirrors <c>GalaxyProxyDriverFactoryExtensions</c>; no dependency on
///     Microsoft.Extensions.DependencyInjection so the driver project stays DI-free.
/// </summary>
/// <remarks>
///     The DriverConfig JSON selects the <see cref="IFocasClientFactory"/> backend:
///     <list type="bullet">
///         <item><c>"Backend": "ipc"</c> (default) — wires <see cref="IpcFocasClientFactory"/>
///         against a named-pipe <see cref="FocasIpcClient"/> talking to a separate
///         <c>Driver.FOCAS.Host</c> process (Tier-C isolation). Requires <c>PipeName</c> +
///         <c>SharedSecret</c>.</item>
///         <item><c>"Backend": "fwlib"</c> — direct in-process Fwlib32.dll P/Invoke via
///         <see cref="FwlibFocasClientFactory"/>. Use only when the main server is licensed
///         for FOCAS and you accept the native-crash blast-radius trade-off.</item>
///         <item><c>"Backend": "unimplemented"</c> — returns the no-op factory; useful for
///         scaffolding DriverInstance rows before the Host is deployed so the server boots.</item>
///     </list>
///     Devices / Tags / Probe / Timeout / Series come from the same JSON and feed directly
///     into <see cref="FocasDriverOptions"/>.
/// </remarks>
public static class FocasDriverFactoryExtensions
{
    public const string DriverTypeName = "FOCAS";

    /// <summary>
    ///     Register the FOCAS driver factory in the supplied <see cref="DriverFactoryRegistry"/>.
    ///     Throws if 'FOCAS' is already registered — single-instance per process.
    /// </summary>
    public static void Register(DriverFactoryRegistry registry)
    {
        ArgumentNullException.ThrowIfNull(registry);
        registry.Register(DriverTypeName, CreateInstance);
    }

    internal static FocasDriver CreateInstance(string driverInstanceId, string driverConfigJson)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
        ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);

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

        // Eager-validate top-level Series so a typo fails fast regardless of whether Devices
        // are populated yet (common during rollout when rows are seeded before CNCs arrive).
        _ = ParseSeries(dto.Series);

        var options = new FocasDriverOptions
        {
            Devices = dto.Devices is { Count: > 0 }
                ? [.. dto.Devices.Select(d => new FocasDeviceOptions(
                    HostAddress: d.HostAddress ?? throw new InvalidOperationException(
                        $"FOCAS config for '{driverInstanceId}' has a device missing HostAddress"),
                    DeviceName: d.DeviceName,
                    Series: ParseSeries(d.Series ?? dto.Series)))]
                : [],
            Tags = dto.Tags is { Count: > 0 }
                ? [.. dto.Tags.Select(t => new FocasTagDefinition(
                    Name: t.Name ?? throw new InvalidOperationException(
                        $"FOCAS config for '{driverInstanceId}' has a tag missing Name"),
                    DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException(
                        $"FOCAS tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"),
                    Address: t.Address ?? throw new InvalidOperationException(
                        $"FOCAS tag '{t.Name}' in '{driverInstanceId}' missing Address"),
                    DataType: ParseDataType(t.DataType, t.Name!, driverInstanceId),
                    Writable: t.Writable ?? true,
                    WriteIdempotent: t.WriteIdempotent ?? false))]
                : [],
            Probe = new FocasProbeOptions
            {
                Enabled = dto.Probe?.Enabled ?? true,
                Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000),
                Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000),
            },
            Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 2_000),
        };

        var clientFactory = BuildClientFactory(dto, driverInstanceId);
        return new FocasDriver(options, driverInstanceId, clientFactory);
    }

    internal static IFocasClientFactory BuildClientFactory(
        FocasDriverConfigDto dto, string driverInstanceId)
    {
        var backend = (dto.Backend ?? "ipc").Trim().ToLowerInvariant();
        return backend switch
        {
            "ipc" => BuildIpcFactory(dto, driverInstanceId),
            "fwlib" or "fwlib32" => new FwlibFocasClientFactory(),
            "unimplemented" or "none" or "stub" => new UnimplementedFocasClientFactory(),
            _ => throw new InvalidOperationException(
                $"FOCAS driver config for '{driverInstanceId}' has unknown Backend '{dto.Backend}'. " +
                "Expected one of: ipc, fwlib, unimplemented."),
        };
    }

    private static IpcFocasClientFactory BuildIpcFactory(
        FocasDriverConfigDto dto, string driverInstanceId)
    {
        var pipeName = dto.PipeName
            ?? throw new InvalidOperationException(
                $"FOCAS driver config for '{driverInstanceId}' missing required PipeName (Tier-C ipc backend)");
        var sharedSecret = dto.SharedSecret
            ?? throw new InvalidOperationException(
                $"FOCAS driver config for '{driverInstanceId}' missing required SharedSecret (Tier-C ipc backend)");
        var connectTimeout = TimeSpan.FromMilliseconds(dto.ConnectTimeoutMs ?? 10_000);
        var series = ParseSeries(dto.Series);

        // Each IFocasClientFactory.Create() call opens a fresh pipe to the Host — matches the
        // driver's one-client-per-device invariant. FocasIpcClient.ConnectAsync is awaited
        // synchronously via GetAwaiter().GetResult() because IFocasClientFactory.Create is a
        // sync contract; the blocking call lands inside FocasDriver.EnsureConnectedAsync,
        // which immediately awaits IFocasClient.ConnectAsync afterwards so the perceived
        // latency is identical to a fully-async factory.
        return new IpcFocasClientFactory(
            ipcClientFactory: () => FocasIpcClient.ConnectAsync(
                pipeName: pipeName,
                sharedSecret: sharedSecret,
                connectTimeout: connectTimeout,
                ct: CancellationToken.None).GetAwaiter().GetResult(),
            series: series);
    }

    private static FocasCncSeries ParseSeries(string? raw)
    {
        if (string.IsNullOrWhiteSpace(raw)) return FocasCncSeries.Unknown;
        return Enum.TryParse<FocasCncSeries>(raw, ignoreCase: true, out var s)
            ? s
            : throw new InvalidOperationException(
                $"FOCAS Series '{raw}' is not one of {string.Join(", ", Enum.GetNames<FocasCncSeries>())}");
    }

    private static FocasDataType ParseDataType(string? raw, string tagName, string driverInstanceId)
    {
        if (string.IsNullOrWhiteSpace(raw))
            throw new InvalidOperationException(
                $"FOCAS tag '{tagName}' in '{driverInstanceId}' missing DataType");
        return Enum.TryParse<FocasDataType>(raw, ignoreCase: true, out var dt)
            ? dt
            : throw new InvalidOperationException(
                $"FOCAS tag '{tagName}' has unknown DataType '{raw}'. " +
                $"Expected one of {string.Join(", ", Enum.GetNames<FocasDataType>())}");
    }

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

    internal sealed class FocasDriverConfigDto
    {
        public string? Backend { get; init; }
        public string? PipeName { get; init; }
        public string? SharedSecret { get; init; }
        public int? ConnectTimeoutMs { get; init; }
        public string? Series { get; init; }
        public int? TimeoutMs { get; init; }
        public List<FocasDeviceDto>? Devices { get; init; }
        public List<FocasTagDto>? Tags { get; init; }
        public FocasProbeDto? Probe { get; init; }
    }

    internal sealed class FocasDeviceDto
    {
        public string? HostAddress { get; init; }
        public string? DeviceName { get; init; }
        public string? Series { get; init; }
    }

    internal sealed class FocasTagDto
    {
        public string? Name { get; init; }
        public string? DeviceHostAddress { get; init; }
        public string? Address { get; init; }
        public string? DataType { get; init; }
        public bool? Writable { get; init; }
        public bool? WriteIdempotent { get; init; }
    }

    internal sealed class FocasProbeDto
    {
        public bool? Enabled { get; init; }
        public int? IntervalMs { get; init; }
        public int? TimeoutMs { get; init; }
    }
}
