using System;
using Google.Protobuf;
using Serilog;

namespace ZB.MOM.WW.LmxProxy.Host.Domain
{
    /// <summary>
    ///     Converts between COM variant objects (boxed .NET types from MxAccess)
    ///     and proto-generated <see cref="Scada.TypedValue"/> messages.
    /// </summary>
    public static class TypedValueConverter
    {
        private static readonly ILogger Log = Serilog.Log.ForContext(typeof(TypedValueConverter));

        /// <summary>
        ///     Converts a COM variant object to a proto TypedValue.
        ///     Returns null (unset TypedValue) for null, DBNull, or VT_EMPTY/VT_NULL.
        /// </summary>
        public static Scada.TypedValue? ToTypedValue(object? value)
        {
            if (value == null || value is DBNull)
                return null;

            switch (value)
            {
                case bool b:
                    return new Scada.TypedValue { BoolValue = b };

                case short s:       // VT_I2 → widened to int32
                    return new Scada.TypedValue { Int32Value = s };

                case int i:         // VT_I4
                    return new Scada.TypedValue { Int32Value = i };

                case long l:        // VT_I8
                    return new Scada.TypedValue { Int64Value = l };

                case ushort us:     // VT_UI2 → widened to int32
                    return new Scada.TypedValue { Int32Value = us };

                case uint ui:       // VT_UI4 → widened to int64 to avoid sign issues
                    return new Scada.TypedValue { Int64Value = ui };

                case ulong ul:      // VT_UI8 → int64, truncation risk
                    if (ul > (ulong)long.MaxValue)
                        Log.Warning("ulong value {Value} exceeds long.MaxValue, truncation will occur", ul);
                    return new Scada.TypedValue { Int64Value = (long)ul };

                case float f:       // VT_R4
                    return new Scada.TypedValue { FloatValue = f };

                case double d:      // VT_R8
                    return new Scada.TypedValue { DoubleValue = d };

                case string str:    // VT_BSTR
                    return new Scada.TypedValue { StringValue = str };

                case DateTime dt:   // VT_DATE → UTC Ticks
                    return new Scada.TypedValue { DatetimeValue = dt.ToUniversalTime().Ticks };

                case decimal dec:   // VT_DECIMAL → double (precision loss)
                    Log.Warning("Decimal value {Value} converted to double, precision loss may occur", dec);
                    return new Scada.TypedValue { DoubleValue = (double)dec };

                case byte[] bytes:  // VT_ARRAY of bytes
                    return new Scada.TypedValue { BytesValue = ByteString.CopyFrom(bytes) };

                case bool[] boolArr:
                {
                    var arr = new Scada.BoolArray();
                    arr.Values.AddRange(boolArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { BoolValues = arr } };
                }

                case int[] intArr:
                {
                    var arr = new Scada.Int32Array();
                    arr.Values.AddRange(intArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { Int32Values = arr } };
                }

                case long[] longArr:
                {
                    var arr = new Scada.Int64Array();
                    arr.Values.AddRange(longArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { Int64Values = arr } };
                }

                case float[] floatArr:
                {
                    var arr = new Scada.FloatArray();
                    arr.Values.AddRange(floatArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { FloatValues = arr } };
                }

                case double[] doubleArr:
                {
                    var arr = new Scada.DoubleArray();
                    arr.Values.AddRange(doubleArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { DoubleValues = arr } };
                }

                case string[] strArr:
                {
                    var arr = new Scada.StringArray();
                    arr.Values.AddRange(strArr);
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { StringValues = arr } };
                }

                case DateTime[] dtArr:
                {
                    var arr = new Scada.DatetimeArray();
                    arr.Values.AddRange(Array.ConvertAll(dtArr, dt => dt.ToUniversalTime().Ticks));
                    return new Scada.TypedValue { ArrayValue = new Scada.ArrayValue { DatetimeValues = arr } };
                }

                default:
                    // VT_UNKNOWN or any unrecognized type — ToString() fallback
                    Log.Warning("Unrecognized COM variant type {Type}, using ToString() fallback", value.GetType().Name);
                    return new Scada.TypedValue { StringValue = value.ToString() };
            }
        }

        /// <summary>
        ///     Converts a proto TypedValue back to a boxed .NET object.
        ///     Returns null for unset oneof (null TypedValue or ValueCase.None).
        /// </summary>
        public static object? FromTypedValue(Scada.TypedValue? typedValue)
        {
            if (typedValue == null)
                return null;

            switch (typedValue.ValueCase)
            {
                case Scada.TypedValue.ValueOneofCase.BoolValue:
                    return typedValue.BoolValue;

                case Scada.TypedValue.ValueOneofCase.Int32Value:
                    return typedValue.Int32Value;

                case Scada.TypedValue.ValueOneofCase.Int64Value:
                    return typedValue.Int64Value;

                case Scada.TypedValue.ValueOneofCase.FloatValue:
                    return typedValue.FloatValue;

                case Scada.TypedValue.ValueOneofCase.DoubleValue:
                    return typedValue.DoubleValue;

                case Scada.TypedValue.ValueOneofCase.StringValue:
                    return typedValue.StringValue;

                case Scada.TypedValue.ValueOneofCase.BytesValue:
                    return typedValue.BytesValue.ToByteArray();

                case Scada.TypedValue.ValueOneofCase.DatetimeValue:
                    return new DateTime(typedValue.DatetimeValue, DateTimeKind.Utc);

                case Scada.TypedValue.ValueOneofCase.ArrayValue:
                    return FromArrayValue(typedValue.ArrayValue);

                case Scada.TypedValue.ValueOneofCase.None:
                default:
                    return null;
            }
        }

        private static object? FromArrayValue(Scada.ArrayValue? arrayValue)
        {
            if (arrayValue == null)
                return null;

            switch (arrayValue.ValuesCase)
            {
                case Scada.ArrayValue.ValuesOneofCase.BoolValues:
                    return arrayValue.BoolValues?.Values?.Count > 0
                        ? ToArray(arrayValue.BoolValues.Values)
                        : Array.Empty<bool>();

                case Scada.ArrayValue.ValuesOneofCase.Int32Values:
                    return arrayValue.Int32Values?.Values?.Count > 0
                        ? ToArray(arrayValue.Int32Values.Values)
                        : Array.Empty<int>();

                case Scada.ArrayValue.ValuesOneofCase.Int64Values:
                    return arrayValue.Int64Values?.Values?.Count > 0
                        ? ToArray(arrayValue.Int64Values.Values)
                        : Array.Empty<long>();

                case Scada.ArrayValue.ValuesOneofCase.FloatValues:
                    return arrayValue.FloatValues?.Values?.Count > 0
                        ? ToArray(arrayValue.FloatValues.Values)
                        : Array.Empty<float>();

                case Scada.ArrayValue.ValuesOneofCase.DoubleValues:
                    return arrayValue.DoubleValues?.Values?.Count > 0
                        ? ToArray(arrayValue.DoubleValues.Values)
                        : Array.Empty<double>();

                case Scada.ArrayValue.ValuesOneofCase.StringValues:
                    return arrayValue.StringValues?.Values?.Count > 0
                        ? ToArray(arrayValue.StringValues.Values)
                        : Array.Empty<string>();

                case Scada.ArrayValue.ValuesOneofCase.DatetimeValues:
                    if (arrayValue.DatetimeValues?.Values?.Count > 0)
                    {
                        var ticks = ToArray(arrayValue.DatetimeValues.Values);
                        var result = new DateTime[ticks.Length];
                        for (int i = 0; i < ticks.Length; i++)
                            result[i] = new DateTime(ticks[i], DateTimeKind.Utc);
                        return result;
                    }
                    return Array.Empty<DateTime>();

                default:
                    return null;
            }
        }

        private static T[] ToArray<T>(Google.Protobuf.Collections.RepeatedField<T> repeatedField)
        {
            var result = new T[repeatedField.Count];
            for (int i = 0; i < repeatedField.Count; i++)
                result[i] = repeatedField[i];
            return result;
        }
    }
}
