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

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
///     then materialises FOCAS DriverInstance rows from the central config DB
///     into live driver instances.
/// </summary>
/// <remarks>
///     The DriverConfig JSON selects the <see cref="IFocasClientFactory"/> backend:
///     <list type="bullet">
///         <item><c>"Backend": "wire"</c> (default) — pure-managed FOCAS2 wire
///         client (<see cref="WireFocasClientFactory"/>) speaking directly to
///         the CNC on TCP:8193.</item>
///         <item><c>"Backend": "unimplemented"</c> / <c>"none"</c> / <c>"stub"</c>
///         — returns the no-op factory; useful for scaffolding DriverInstance
///         rows before the CNC endpoint is reachable.</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>
    /// <param name="registry">The driver factory registry to register with.</param>
    public static void Register(DriverFactoryRegistry registry)
    {
        ArgumentNullException.ThrowIfNull(registry);
        registry.Register(DriverTypeName, CreateInstance);
    }

    /// <summary>
    ///     Creates a new <see cref="FocasDriver"/> instance from the supplied configuration ID and JSON.
    /// </summary>
    /// <param name="driverInstanceId">The unique driver instance identifier.</param>
    /// <param name="driverConfigJson">The driver configuration JSON string.</param>
    /// <returns>A configured <see cref="FocasDriver"/> instance.</returns>
    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),
            FixedTree = BuildFixedTree(dto.FixedTree),
            AlarmProjection = BuildAlarmProjection(dto.AlarmProjection),
            HandleRecycle = BuildHandleRecycle(dto.HandleRecycle),
        };

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

    /// <summary>
    ///     Builds the appropriate <see cref="IFocasClientFactory"/> based on the config DTO's backend selection.
    /// </summary>
    /// <param name="dto">The driver configuration DTO.</param>
    /// <param name="driverInstanceId">The driver instance identifier for error reporting.</param>
    /// <returns>A configured <see cref="IFocasClientFactory"/> instance.</returns>
    internal static IFocasClientFactory BuildClientFactory(
        FocasDriverConfigDto dto, string driverInstanceId)
    {
        var backend = (dto.Backend ?? "wire").Trim().ToLowerInvariant();
        return backend switch
        {
            "wire" => new WireFocasClientFactory(),
            "unimplemented" or "none" or "stub" => new UnimplementedFocasClientFactory(),
            _ => throw new InvalidOperationException(
                $"FOCAS driver config for '{driverInstanceId}' has unknown Backend '{dto.Backend}'. " +
                "Expected one of: wire, unimplemented. " +
                "(The legacy 'ipc' / 'fwlib' backends were retired in the Wire migration — " +
                "see docs/drivers/FOCAS.md.)"),
        };
    }

    /// <summary>
    ///     Map the optional <c>FixedTree</c> config section onto <see cref="FocasFixedTreeOptions"/>.
    ///     A missing section keeps the hard-coded defaults (tree disabled); a present section
    ///     with omitted intervals keeps each interval's default. Interval fields are JSON
    ///     <see cref="TimeSpan"/> strings (e.g. <c>"00:00:00.250"</c>) per docs/drivers/FOCAS.md.
    /// </summary>
    private static FocasFixedTreeOptions BuildFixedTree(FocasFixedTreeDto? dto)
    {
        if (dto is null) return new FocasFixedTreeOptions();
        var defaults = new FocasFixedTreeOptions();
        return new FocasFixedTreeOptions
        {
            Enabled = dto.Enabled ?? defaults.Enabled,
            PollInterval = dto.PollInterval ?? defaults.PollInterval,
            ProgramPollInterval = dto.ProgramPollInterval ?? defaults.ProgramPollInterval,
            TimerPollInterval = dto.TimerPollInterval ?? defaults.TimerPollInterval,
        };
    }

    /// <summary>
    ///     Map the optional <c>AlarmProjection</c> config section onto
    ///     <see cref="FocasAlarmProjectionOptions"/>. Missing section keeps the disabled default.
    /// </summary>
    private static FocasAlarmProjectionOptions BuildAlarmProjection(FocasAlarmProjectionDto? dto)
    {
        if (dto is null) return new FocasAlarmProjectionOptions();
        var defaults = new FocasAlarmProjectionOptions();
        return new FocasAlarmProjectionOptions
        {
            Enabled = dto.Enabled ?? defaults.Enabled,
            PollInterval = dto.PollInterval ?? defaults.PollInterval,
        };
    }

    /// <summary>
    ///     Map the optional <c>HandleRecycle</c> config section onto
    ///     <see cref="FocasHandleRecycleOptions"/>. Missing section keeps the disabled default.
    /// </summary>
    private static FocasHandleRecycleOptions BuildHandleRecycle(FocasHandleRecycleDto? dto)
    {
        if (dto is null) return new FocasHandleRecycleOptions();
        var defaults = new FocasHandleRecycleOptions();
        return new FocasHandleRecycleOptions
        {
            Enabled = dto.Enabled ?? defaults.Enabled,
            Interval = dto.Interval ?? defaults.Interval,
        };
    }

    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
    {
        /// <summary>Gets or sets the FOCAS client factory backend name (e.g. "wire" or "stub").</summary>
        public string? Backend { get; init; }

        /// <summary>Gets or sets the CNC series for this driver.</summary>
        public string? Series { get; init; }

        /// <summary>Gets or sets the operation timeout in milliseconds.</summary>
        public int? TimeoutMs { get; init; }

        /// <summary>Gets or sets the list of CNC devices to manage.</summary>
        public List<FocasDeviceDto>? Devices { get; init; }

        /// <summary>Gets or sets the list of FOCAS tags to poll.</summary>
        public List<FocasTagDto>? Tags { get; init; }

        /// <summary>Gets or sets the probe configuration options.</summary>
        public FocasProbeDto? Probe { get; init; }

        /// <summary>Gets or sets the fixed-tree configuration options.</summary>
        public FocasFixedTreeDto? FixedTree { get; init; }

        /// <summary>Gets or sets the alarm projection configuration options.</summary>
        public FocasAlarmProjectionDto? AlarmProjection { get; init; }

        /// <summary>Gets or sets the handle recycle configuration options.</summary>
        public FocasHandleRecycleDto? HandleRecycle { get; init; }
    }

    internal sealed class FocasDeviceDto
    {
        /// <summary>Gets or sets the hostname or IP address of the CNC.</summary>
        public string? HostAddress { get; init; }

        /// <summary>Gets or sets the logical device name.</summary>
        public string? DeviceName { get; init; }

        /// <summary>Gets or sets the CNC series for this device (overrides top-level series if provided).</summary>
        public string? Series { get; init; }
    }

    internal sealed class FocasTagDto
    {
        /// <summary>Gets or sets the tag name.</summary>
        public string? Name { get; init; }

        /// <summary>Gets or sets the hostname or IP address of the CNC device for this tag.</summary>
        public string? DeviceHostAddress { get; init; }

        /// <summary>Gets or sets the FOCAS address or path for this tag.</summary>
        public string? Address { get; init; }

        /// <summary>Gets or sets the data type for this tag.</summary>
        public string? DataType { get; init; }

        /// <summary>Gets or sets a value indicating whether this tag is writable.</summary>
        public bool? Writable { get; init; }

        /// <summary>Gets or sets a value indicating whether writes to this tag are idempotent.</summary>
        public bool? WriteIdempotent { get; init; }
    }

    internal sealed class FocasProbeDto
    {
        /// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
        public bool? Enabled { get; init; }

        /// <summary>Gets or sets the probe interval in milliseconds.</summary>
        public int? IntervalMs { get; init; }

        /// <summary>Gets or sets the probe timeout in milliseconds.</summary>
        public int? TimeoutMs { get; init; }
    }

    /// <summary>
    ///     Optional <c>FixedTree</c> config section. Interval fields are JSON
    ///     <see cref="TimeSpan"/> strings (<c>"hh:mm:ss[.fff]"</c>) — System.Text.Json
    ///     parses these natively.
    /// </summary>
    internal sealed class FocasFixedTreeDto
    {
        /// <summary>Gets or sets a value indicating whether fixed-tree discovery is enabled.</summary>
        public bool? Enabled { get; init; }

        /// <summary>Gets or sets the poll interval for discovering fixed-tree items.</summary>
        public TimeSpan? PollInterval { get; init; }

        /// <summary>Gets or sets the poll interval for discovering program-related items.</summary>
        public TimeSpan? ProgramPollInterval { get; init; }

        /// <summary>Gets or sets the poll interval for discovering timer-related items.</summary>
        public TimeSpan? TimerPollInterval { get; init; }
    }

    /// <summary>Optional <c>AlarmProjection</c> config section.</summary>
    internal sealed class FocasAlarmProjectionDto
    {
        /// <summary>Gets or sets a value indicating whether alarm projection is enabled.</summary>
        public bool? Enabled { get; init; }

        /// <summary>Gets or sets the alarm poll interval.</summary>
        public TimeSpan? PollInterval { get; init; }
    }

    /// <summary>Optional <c>HandleRecycle</c> config section.</summary>
    internal sealed class FocasHandleRecycleDto
    {
        /// <summary>Gets or sets a value indicating whether handle recycling is enabled.</summary>
        public bool? Enabled { get; init; }

        /// <summary>Gets or sets the handle recycle interval.</summary>
        public TimeSpan? Interval { get; init; }
    }
}
