using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Worker.Conversion;

/// <summary>Converts MXAccess MXSTATUS_PROXY COM objects to protobuf MxStatusProxy messages.</summary>
public sealed class MxStatusProxyConverter
{
    /// <summary>
    ///     Per-type cache of the four field accessors a status conversion needs.
    ///     The status type is stable (the interop <c>MXSTATUS_PROXY</c> struct in
    ///     production; a fixed test double in tests), so the expensive
    ///     <see cref="Type.GetField(string, BindingFlags)"/> metadata scan — and,
    ///     for a visible type, the one-time expression compile that replaces
    ///     <see cref="FieldInfo.GetValue(object)"/> on the event path — happens
    ///     once per type and is reused. Keyed by <see cref="Type"/> so a
    ///     plain-CLR test double and the real interop struct each get their own
    ///     entry, keeping the converter interop-agnostic.
    /// </summary>
    private static readonly ConcurrentDictionary<Type, StatusFields> FieldCache = new();

    /// <summary>Converts a single status object to a protobuf message, reflecting all fields and diagnostics.</summary>
    /// <param name="status">COM status object to convert.</param>
    /// <returns>The converted protobuf status message.</returns>
    public MxStatusProxy Convert(object status)
    {
        if (status is null)
        {
            throw new ArgumentNullException(nameof(status));
        }

        StatusFields fields = GetFields(status.GetType());
        int success = fields.Success(status);
        int rawCategory = fields.Category(status);
        int rawDetectedBy = fields.DetectedBy(status);
        int detail = fields.Detail(status);

        return new MxStatusProxy
        {
            Success = success,
            Category = MapCategory(rawCategory),
            DetectedBy = MapSource(rawDetectedBy),
            Detail = detail,
            RawCategory = rawCategory,
            RawDetectedBy = rawDetectedBy,
            DiagnosticText = MxStatusDetailText.Lookup(detail),
        };
    }

    /// <summary>Converts an array of status objects, handling nulls gracefully.</summary>
    /// <param name="statuses">Array of COM status objects; null returns empty list.</param>
    /// <returns>The converted protobuf status messages, one per input element (null entries become an "Unknown" placeholder).</returns>
    public IReadOnlyList<MxStatusProxy> ConvertMany(Array? statuses)
    {
        if (statuses is null)
        {
            return Array.Empty<MxStatusProxy>();
        }

        List<MxStatusProxy> converted = new(statuses.Length);
        foreach (object? status in statuses)
        {
            if (status is null)
            {
                converted.Add(new MxStatusProxy
                {
                    Category = MxStatusCategory.Unknown,
                    DetectedBy = MxStatusSource.Unknown,
                    DiagnosticText = "Null MXSTATUS_PROXY entry.",
                });
                continue;
            }

            converted.Add(Convert(status));
        }

        return converted;
    }

    /// <summary>Preserves completion-only status bytes as a diagnostic hex string since they cannot be unpacked.</summary>
    /// <param name="statusBytes">Status bytes to encode as hex string.</param>
    /// <returns>The diagnostic hex-encoded representation of <paramref name="statusBytes"/>.</returns>
    public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes)
    {
        if (statusBytes is null)
        {
            throw new ArgumentNullException(nameof(statusBytes));
        }

        return $"completion_only_status_hex={BitConverter.ToString(statusBytes).Replace("-", string.Empty)}";
    }

    private static int ReadInt32Field(
        object value,
        Type valueType,
        FieldInfo field)
    {
        object? fieldValue = field.GetValue(value);
        if (fieldValue is null)
        {
            throw new MxStatusConversionException(
                $"Status object field '{field.Name}' on type '{valueType.FullName}' is null.");
        }

        return System.Convert.ToInt32(fieldValue, CultureInfo.InvariantCulture);
    }

    /// <summary>
    ///     Resolves (and caches) the four field accessors for the given status
    ///     type. The first resolution for a type performs the reflection scan and
    ///     the expression compile; every subsequent conversion of that type
    ///     reuses the cached delegates. A type missing a required field throws the
    ///     same <see cref="MxStatusConversionException"/> the per-field lookup used
    ///     to throw — and, because <see cref="ConcurrentDictionary{TKey,TValue}.GetOrAdd(TKey, Func{TKey, TValue})"/>
    ///     does not store a value when the factory throws, a bad type keeps
    ///     failing identically on every call rather than being cached.
    /// </summary>
    /// <param name="statusType">Runtime type of the status object being converted.</param>
    /// <returns>The resolved field accessors for <paramref name="statusType"/>.</returns>
    private static StatusFields GetFields(Type statusType)
    {
        return FieldCache.GetOrAdd(
            statusType,
            type => new StatusFields(
                BuildAccessor(type, "success"),
                BuildAccessor(type, "category"),
                BuildAccessor(type, "detectedBy"),
                BuildAccessor(type, "detail")));
    }

    /// <summary>
    ///     Builds the accessor for one status field. Every OnDataChange carries a
    ///     status array, so this read is on the hot event path: a public field on
    ///     a publicly visible type is read through a delegate compiled once from
    ///     an expression tree, which drops the boxed <see cref="object"/> that
    ///     <see cref="FieldInfo.GetValue(object)"/> allocates per field per status
    ///     entry, plus the <see cref="IConvertible"/> dispatch behind
    ///     <see cref="System.Convert.ToInt32(object, IFormatProvider)"/>.
    ///     <para>
    ///         The value is required to be identical, so the compiled path is
    ///         taken only for field types whose conversion to <see cref="int"/>
    ///         is a lossless widening (see <see cref="IsWideningToInt32"/>) —
    ///         the interop <c>MXSTATUS_PROXY</c> declares its four fields as
    ///         16/32-bit integers. Field types where the CLR conversion and
    ///         <c>Convert.ToInt32</c> can disagree (bool, char, floating point,
    ///         unsigned 32/64-bit, or a reference type that could be null) keep
    ///         the reflection read verbatim, as does a non-visible type, whose
    ///         members a compiled delegate is not permitted to touch.
    ///     </para>
    /// </summary>
    /// <param name="valueType">Runtime type of the status object.</param>
    /// <param name="fieldName">Name of the status field to read.</param>
    /// <returns>A delegate reading the field as an <see cref="int"/>.</returns>
    private static Func<object, int> BuildAccessor(
        Type valueType,
        string fieldName)
    {
        FieldInfo field = ResolveField(valueType, fieldName);
        if (valueType.IsVisible && IsWideningToInt32(field.FieldType))
        {
            try
            {
                ParameterExpression parameter = Expression.Parameter(typeof(object), "status");

                // Unbox (not Convert) for a struct: it addresses the boxed
                // instance in place rather than copying it out before the load.
                Expression instance = valueType.IsValueType
                    ? Expression.Unbox(parameter, valueType)
                    : Expression.Convert(parameter, valueType);
                Expression widened = Expression.Convert(Expression.Field(instance, field), typeof(int));
                return Expression.Lambda<Func<object, int>>(widened, parameter).Compile();
            }
            catch (Exception exception) when (exception is not OutOfMemoryException)
            {
                // Compiling the accessor is an optimization, never a requirement:
                // if the runtime refuses it the reflection read below still
                // produces the same value.
            }
        }

        return status => ReadInt32Field(status, valueType, field);
    }

    /// <summary>
    ///     Reports whether reading a field of this type as an <see cref="int"/>
    ///     via the CLR's own conversion is lossless and total, and therefore
    ///     indistinguishable from <see cref="System.Convert.ToInt32(object, IFormatProvider)"/>.
    ///     Enums answer with their underlying type code, which is the intent.
    /// </summary>
    /// <param name="fieldType">Declared type of the status field.</param>
    /// <returns><see langword="true"/> when the compiled accessor is safe to use.</returns>
    private static bool IsWideningToInt32(Type fieldType)
    {
        switch (Type.GetTypeCode(fieldType))
        {
            case TypeCode.SByte:
            case TypeCode.Byte:
            case TypeCode.Int16:
            case TypeCode.UInt16:
            case TypeCode.Int32:
                return true;
            default:
                return false;
        }
    }

    private static FieldInfo ResolveField(
        Type valueType,
        string fieldName)
    {
        FieldInfo? field = valueType.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public);
        if (field is null)
        {
            throw new MxStatusConversionException(
                $"Status object type '{valueType.FullName}' does not expose required field '{fieldName}'.");
        }

        return field;
    }

    private static MxStatusCategory MapCategory(int rawCategory)
    {
        return rawCategory switch
        {
            -1 => MxStatusCategory.Unknown,
            0 => MxStatusCategory.Ok,
            1 => MxStatusCategory.Pending,
            2 => MxStatusCategory.Warning,
            3 => MxStatusCategory.CommunicationError,
            4 => MxStatusCategory.ConfigurationError,
            5 => MxStatusCategory.OperationalError,
            6 => MxStatusCategory.SecurityError,
            7 => MxStatusCategory.SoftwareError,
            8 => MxStatusCategory.OtherError,
            _ => MxStatusCategory.Unknown,
        };
    }

    private static MxStatusSource MapSource(int rawDetectedBy)
    {
        return rawDetectedBy switch
        {
            -1 => MxStatusSource.Unknown,
            0 => MxStatusSource.RequestingLmx,
            1 => MxStatusSource.RespondingLmx,
            2 => MxStatusSource.RequestingNmx,
            3 => MxStatusSource.RespondingNmx,
            4 => MxStatusSource.RequestingAutomationObject,
            5 => MxStatusSource.RespondingAutomationObject,
            _ => MxStatusSource.Unknown,
        };
    }

    /// <summary>
    ///     The four resolved status-field accessors cached per type. Plain
    ///     readonly struct (not a record) so it compiles under the worker's net48
    ///     target, which lacks <c>IsExternalInit</c>.
    /// </summary>
    private readonly struct StatusFields
    {
        public StatusFields(
            Func<object, int> success,
            Func<object, int> category,
            Func<object, int> detectedBy,
            Func<object, int> detail)
        {
            Success = success;
            Category = category;
            DetectedBy = detectedBy;
            Detail = detail;
        }

        public Func<object, int> Success { get; }

        public Func<object, int> Category { get; }

        public Func<object, int> DetectedBy { get; }

        public Func<object, int> Detail { get; }
    }
}
