using System.Collections;
using System.Globalization;
using System.Text.Json;

namespace ZB.MOM.WW.ScadaBridge.Commons.Types;

/// <summary>
/// Typed wrapper around script parameters. Implements IReadOnlyDictionary for backward
/// compatibility (Parameters["key"]) and adds Get&lt;T&gt;() for typed access with
/// clear error messages.
/// </summary>
public class ScriptParameters : IReadOnlyDictionary<string, object?>
{
    private readonly IReadOnlyDictionary<string, object?> _inner;

    /// <summary>
    /// Initializes a new instance of the <see cref="ScriptParameters"/> class with the specified parameters.
    /// </summary>
    /// <param name="parameters">The underlying parameter dictionary.</param>
    public ScriptParameters(IReadOnlyDictionary<string, object?> parameters)
    {
        _inner = parameters ?? throw new ArgumentNullException(nameof(parameters));
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ScriptParameters"/> class with an empty parameter dictionary.
    /// </summary>
    public ScriptParameters() : this(new Dictionary<string, object?>()) { }

    /// <summary>
    /// Gets a parameter value with typed conversion.
    /// <list type="bullet">
    ///   <item><c>Get&lt;int&gt;("key")</c> — throws if missing, null, or unconvertible.</item>
    ///   <item><c>Get&lt;int?&gt;("key")</c> — returns null if the parameter is missing or null;
    ///   throws if it is present but holds an unconvertible value.</item>
    ///   <item><c>Get&lt;int[]&gt;("key")</c> — converts list to typed array; throws on first bad element.</item>
    ///   <item><c>Get&lt;List&lt;int&gt;&gt;("key")</c> — converts list to typed List; throws on first bad element.</item>
    /// </list>
    /// </summary>
    /// <typeparam name="T">The target type for the parameter value.</typeparam>
    /// <param name="key">The parameter key.</param>
    /// <returns>The converted parameter value.</returns>
    public T Get<T>(string key)
    {
        var targetType = typeof(T);

        // Array types: int[], string[], etc.
        if (targetType.IsArray && targetType != typeof(string))
        {
            var elementType = targetType.GetElementType()!;
            return (T)(object)ConvertToArray(key, elementType);
        }

        // List<T> types
        if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(List<>))
        {
            var elementType = targetType.GetGenericArguments()[0];
            return (T)ConvertToList(key, elementType);
        }

        // Nullable<T> types: int?, bool?, etc.
        var underlyingType = Nullable.GetUnderlyingType(targetType);
        if (underlyingType != null)
        {
            return GetNullable<T>(key, underlyingType);
        }

        // Non-nullable scalar types
        return GetRequired<T>(key, targetType);
    }

    private T GetRequired<T>(string key, Type targetType)
    {
        if (!_inner.TryGetValue(key, out var value))
            throw new ScriptParameterException($"Parameter '{key}' not found");

        if (value is null)
            throw new ScriptParameterException($"Parameter '{key}' value is null");

        return (T)ConvertScalar(value, targetType, key);
    }

    private T GetNullable<T>(string key, Type underlyingType)
    {
        // Absent or explicitly-null parameter — the caller did not supply a value.
        if (!_inner.TryGetValue(key, out var value) || value is null)
            return default!; // null for Nullable<T>

        // A parameter that is *present but non-null* must be convertible. A value
        // that cannot be converted is a caller/script bug, not "not supplied":
        // throw with a descriptive message rather than silently returning null
        // (which a script would misread as absent). This mirrors Get<T>() and the
        // array/list element paths.
        var converted = ConvertScalar(value, underlyingType, key);
        return (T)converted;
    }

    private Array ConvertToArray(string key, Type elementType)
    {
        var list = GetSourceList(key);
        var array = Array.CreateInstance(elementType, list.Count);
        for (var i = 0; i < list.Count; i++)
        {
            var item = list[i];
            if (item is null)
                throw new ScriptParameterException(
                    $"Parameter '{key}' element at index {i} is null");

            try
            {
                array.SetValue(ConvertScalar(item, elementType, key), i);
            }
            catch (ScriptParameterException)
            {
                throw new ScriptParameterException(
                    $"Parameter '{key}' element at index {i} with value '{item}' could not be parsed as {elementType.Name}");
            }
        }
        return array;
    }

    private object ConvertToList(string key, Type elementType)
    {
        var list = GetSourceList(key);
        var listType = typeof(List<>).MakeGenericType(elementType);
        var result = (IList)Activator.CreateInstance(listType, list.Count)!;
        for (var i = 0; i < list.Count; i++)
        {
            var item = list[i];
            if (item is null)
                throw new ScriptParameterException(
                    $"Parameter '{key}' element at index {i} is null");

            try
            {
                result.Add(ConvertScalar(item, elementType, key));
            }
            catch (ScriptParameterException)
            {
                throw new ScriptParameterException(
                    $"Parameter '{key}' element at index {i} with value '{item}' could not be parsed as {elementType.Name}");
            }
        }
        return result;
    }

    private IList GetSourceList(string key)
    {
        if (!_inner.TryGetValue(key, out var value))
            throw new ScriptParameterException($"Parameter '{key}' not found");

        if (value is null)
            throw new ScriptParameterException($"Parameter '{key}' value is null");

        if (value is IList list)
            return list;

        throw new ScriptParameterException($"Parameter '{key}' is not a list or array");
    }

    private static object ConvertScalar(object value, Type targetType, string key)
    {
        // Direct type match
        if (targetType.IsInstanceOfType(value))
            return value;

        // Unwrap JsonElement (from JSON deserialization of List<object?> / Dictionary<string, object?>)
        if (value is JsonElement je)
            return ConvertJsonElement(je, targetType, key);

        // string target — use ToString
        if (targetType == typeof(string))
            return value.ToString() ?? string.Empty;

        // DateTime from string
        if (targetType == typeof(DateTime) && value is string dateStr)
        {
            if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
                    DateTimeStyles.RoundtripKind, out var dt))
                return dt;

            throw new ScriptParameterException(
                $"Parameter '{key}' with value '{value}' could not be parsed as DateTime");
        }

        // Numeric and other IConvertible conversions
        try
        {
            return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
        }
        catch (Exception ex) when (ex is InvalidCastException or FormatException or OverflowException)
        {
            throw new ScriptParameterException(
                $"Parameter '{key}' with value '{value}' could not be parsed as {targetType.Name}");
        }
    }

    private static object ConvertJsonElement(JsonElement element, Type targetType, string key)
    {
        try
        {
            if (targetType == typeof(bool) && element.ValueKind is JsonValueKind.True or JsonValueKind.False)
                return element.GetBoolean();
            if (targetType == typeof(int) && element.ValueKind == JsonValueKind.Number)
                return element.GetInt32();
            if (targetType == typeof(long) && element.ValueKind == JsonValueKind.Number)
                return element.GetInt64();
            if (targetType == typeof(float) && element.ValueKind == JsonValueKind.Number)
                return element.GetSingle();
            if (targetType == typeof(double) && element.ValueKind == JsonValueKind.Number)
                return element.GetDouble();
            if (targetType == typeof(string) && element.ValueKind == JsonValueKind.String)
                return element.GetString()!;
            if (targetType == typeof(string))
                return element.ToString();
            if (targetType == typeof(DateTime) && element.ValueKind == JsonValueKind.String)
            {
                if (DateTime.TryParse(element.GetString(), CultureInfo.InvariantCulture,
                        DateTimeStyles.RoundtripKind, out var dt))
                    return dt;
            }
        }
        catch (Exception ex) when (ex is FormatException or OverflowException or InvalidOperationException)
        {
            // Fall through to error
        }

        throw new ScriptParameterException(
            $"Parameter '{key}' with value '{element}' could not be parsed as {targetType.Name}");
    }

    // IReadOnlyDictionary<string, object?> implementation
    /// <summary>
    /// Gets the value associated with the specified key.
    /// </summary>
    /// <param name="key">The parameter key.</param>
    /// <returns>The parameter value, or null if the key is not found.</returns>
    public object? this[string key] => _inner[key];
    /// <summary>
    /// Gets the collection of parameter keys.
    /// </summary>
    public IEnumerable<string> Keys => _inner.Keys;
    /// <summary>
    /// Gets the collection of parameter values.
    /// </summary>
    public IEnumerable<object?> Values => _inner.Values;
    /// <summary>
    /// Gets the number of parameters.
    /// </summary>
    public int Count => _inner.Count;
    /// <summary>
    /// Determines whether the parameters contain the specified key.
    /// </summary>
    /// <param name="key">The key to locate.</param>
    /// <returns>True if the key is found; false otherwise.</returns>
    public bool ContainsKey(string key) => _inner.ContainsKey(key);
    /// <summary>
    /// Attempts to get the value associated with the specified key.
    /// </summary>
    /// <param name="key">The key to locate.</param>
    /// <param name="value">The value associated with the key if found; otherwise null.</param>
    /// <returns>True if the key is found; false otherwise.</returns>
    public bool TryGetValue(string key, out object? value) => _inner.TryGetValue(key, out value);
    /// <summary>
    /// Returns an enumerator that iterates through the parameters.
    /// </summary>
    /// <returns>An enumerator for the parameters.</returns>
    public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _inner.GetEnumerator();
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

/// <summary>
/// Exception thrown by <see cref="ScriptParameters.Get{T}"/> when a parameter
/// cannot be retrieved or converted to the requested type.
/// </summary>
public class ScriptParameterException : Exception
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ScriptParameterException"/> class with a specified error message.
    /// </summary>
    /// <param name="message">The error message that explains the reason for the exception.</param>
    public ScriptParameterException(string message) : base(message) { }
    /// <summary>
    /// Initializes a new instance of the <see cref="ScriptParameterException"/> class with a specified error message and inner exception.
    /// </summary>
    /// <param name="message">The error message that explains the reason for the exception.</param>
    /// <param name="innerException">The exception that is the cause of the current exception.</param>
    public ScriptParameterException(string message, Exception innerException)
        : base(message, innerException) { }
}
