// Copyright (C) 2015-2026 The Neo Project.
//
// ParameterConverter.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Cryptography.ECC;
using Neo.Extensions;
using Neo.Json;
using Neo.Network.P2P.Payloads;
using Neo.Plugins.RpcServer.Model;
using Neo.SmartContract;
using Neo.Wallets;
using System.Numerics;
using JToken = Neo.Json.JToken;

namespace Neo.Plugins.RpcServer;

public static class ParameterConverter
{
    private static readonly Dictionary<Type, Func<JToken, object>> s_conversions;

    static ParameterConverter()
    {
        // ToAddress, ToSignersAndWitnesses are registered in RpcServer.cs
        // Because they need a extra parameter(address version).
        s_conversions = new Dictionary<Type, Func<JToken, object>>
        {
            { typeof(string), token => Result.Ok_Or(token.AsString, CreateInvalidParamError<string>(token)) },
            { typeof(byte), ToNumeric<byte> },
            { typeof(sbyte), ToNumeric<sbyte> },
            { typeof(short), ToNumeric<short> },
            { typeof(ushort), ToNumeric<ushort> },
            { typeof(int), ToNumeric<int> },
            { typeof(uint), ToNumeric<uint> },
            { typeof(long), ToNumeric<long> },
            { typeof(ulong), ToNumeric<ulong> },
            { typeof(double), token => Result.Ok_Or(token.AsNumber, CreateInvalidParamError<double>(token)) },
            { typeof(bool), token => Result.Ok_Or(token.AsBoolean, CreateInvalidParamError<bool>(token)) },
            { typeof(byte[]), ToBytes }, // byte[] in jsonrpc request must be base64 encoded.
            { typeof(Guid), ToGuid },
            { typeof(UInt160), ToUInt160 }, // hex-encoded UInt160
            { typeof(UInt256), ToUInt256 }, // hex-encoded UInt256
            { typeof(ContractNameOrHashOrId), ToContractNameOrHashOrId },
            { typeof(BlockHashOrIndex), ToBlockHashOrIndex },
            { typeof(ContractParameter[]), ToContractParameters }
        };
    }

    /// <summary>
    /// Registers a conversion function for a specific type.
    /// If a convert method needs more than one parameter, use a lambda expression to pass the parameters.
    /// </summary>
    /// <typeparam name="T">The type to register the conversion function for.</typeparam>
    /// <param name="conversion">The conversion function to register.</param>
    internal static void RegisterConversion<T>(Func<JToken, object> conversion)
    {
        s_conversions[typeof(T)] = token => conversion(token);
    }

    internal static object AsParameter(this JToken token, Type targetType)
    {
        if (s_conversions.TryGetValue(targetType, out var conversion))
            return conversion(token);
        throw new RpcException(RpcError.InvalidParams.WithData($"Unsupported parameter type: {targetType}"));
    }

    internal static T AsParameter<T>(this JToken token)
    {
        if (s_conversions.TryGetValue(typeof(T), out var conversion))
            return (T)conversion(token);
        throw new RpcException(RpcError.InvalidParams.WithData($"Unsupported parameter type: {typeof(T)}"));
    }

    private static object ToNumeric<T>(JToken token) where T : struct, IMinMaxValue<T>
    {
        if (token is null) throw new RpcException(RpcError.InvalidParams.WithData($"Invalid {typeof(T)}: {token}"));

        if (TryToDoubleToNumericType<T>(token, out var result)) return result;

        throw new RpcException(CreateInvalidParamError<T>(token));
    }

    private static bool TryToDoubleToNumericType<T>(JToken token, out T result) where T : struct, IMinMaxValue<T>
    {
        result = default;
        try
        {
            var value = token.AsNumber();
            var minValue = Convert.ToDouble(T.MinValue);
            var maxValue = Convert.ToDouble(T.MaxValue);
            if (value < minValue || value > maxValue)
            {
                return false;
            }

            if (!typeof(T).IsFloatingPoint() && !IsValidInteger(value))
            {
                return false;
            }

            result = (T)Convert.ChangeType(value, typeof(T));
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsValidInteger(double value)
    {
        // Integer values are safe if they are within the range of MIN_SAFE_INTEGER and MAX_SAFE_INTEGER
        if (value < JNumber.MIN_SAFE_INTEGER || value > JNumber.MAX_SAFE_INTEGER)
            return false;
        return Math.Abs(value % 1) <= double.Epsilon;
    }

    private static object ToUInt160(JToken token)
    {
        if (token is null || token is not JString value)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid UInt160: {token}"));

        if (UInt160.TryParse(value.Value, out var scriptHash)) return scriptHash;

        throw new RpcException(RpcError.InvalidParams.WithData($"Invalid UInt160: {token}"));
    }

    private static object ToUInt256(JToken token)
    {
        if (token is null || token is not JString value)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid UInt256: {token}"));

        if (UInt256.TryParse(value.Value, out var hash)) return hash;

        throw new RpcException(RpcError.InvalidParams.WithData($"Invalid UInt256: {token}"));
    }

    private static object ToBytes(JToken token)
    {
        if (token is not JString value)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid base64-encoded bytes: {token}"));

        return Result.Ok_Or(() => Convert.FromBase64String(value.Value),
            RpcError.InvalidParams.WithData($"Invalid Base64-encoded bytes: {token}"));
    }

    private static object ToContractNameOrHashOrId(JToken token)
    {
        if (ContractNameOrHashOrId.TryParse(token.AsString(), out var contractNameOrHashOrId))
        {
            return contractNameOrHashOrId;
        }
        throw new RpcException(RpcError.InvalidParams.WithData($"Invalid contract hash or id format: {token}"));
    }

    private static object ToBlockHashOrIndex(JToken token)
    {
        if (token is null) throw new RpcException(RpcError.InvalidParams.WithData($"Invalid BlockHashOrIndex: {token}"));

        if (BlockHashOrIndex.TryParse(token.AsString(), out var blockHashOrIndex)) return blockHashOrIndex;

        throw new RpcException(RpcError.InvalidParams.WithData($"Invalid block hash or index format: {token}"));
    }

    private static RpcError CreateInvalidParamError<T>(JToken token)
    {
        return RpcError.InvalidParams.WithData($"Invalid {typeof(T)} value: {token}");
    }

    /// <summary>
    /// Create a SignersAndWitnesses from a JSON array.
    /// Each item in the JSON array should be a JSON object with the following properties:
    /// - "signer": A JSON object with the following properties:
    ///   - "account": A hex-encoded UInt160 or a Base58Check address, required.
    ///   - "scopes": A enum string representing the scopes(WitnessScope) of the signer, required.
    ///   - "allowedcontracts": An array of hex-encoded UInt160, optional.
    ///   - "allowedgroups": An array of hex-encoded ECPoint, optional.
    ///   - "rules": An array of strings representing the rules(WitnessRule) of the signer, optional.
    /// - "witness": A JSON object with the following properties:
    ///   - "invocation": A base64-encoded string representing the invocation script, optional.
    ///   - "verification": A base64-encoded string representing the verification script, optional.
    /// </summary>
    /// <param name="json">The JSON array to create a SignersAndWitnesses from.</param>
    /// <param name="addressVersion">The address version to use for the signers.</param>
    /// <returns>A SignersAndWitnesses object.</returns>
    /// <exception cref="RpcException">Thrown when the JSON array is invalid.</exception>
    internal static SignersAndWitnesses ToSignersAndWitnesses(this JToken json, byte addressVersion)
    {
        if (json is null) return default;
        if (json is not JArray array)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid SignersAndWitnesses: {json}"));

        var signers = array.ToSigners(addressVersion);
        var witnesses = array.ToWitnesses();
        return new(signers, witnesses);
    }

    /// <summary>
    /// Create a Signer from a JSON object.
    /// The JSON object should have the following properties:
    /// - "account": A hex-encoded UInt160 or a Base58Check address, required.
    /// - "scopes": A enum string representing the scopes(WitnessScope) of the signer, required.
    /// - "allowedcontracts": An array of hex-encoded UInt160, optional.
    /// - "allowedgroups": An array of hex-encoded ECPoint, optional.
    /// - "rules": An array of strings representing the rules(WitnessRule) of the signer, optional.
    /// </summary>
    /// <param name="json">The JSON object to create a Signer from.</param>
    /// <param name="addressVersion">The address version to use for the signer.</param>
    /// <returns>A Signer object.</returns>
    /// <exception cref="RpcException">Thrown when the JSON object is invalid.</exception>
    internal static Signer ToSigner(this JToken json, byte addressVersion)
    {
        if (json is null || json is not JObject obj) throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Signer: {json}"));

        var account = obj["account"].NotNull_Or(RpcError.InvalidParams.WithData($"Invalid 'account' in Signer."));
        var scopes = obj["scopes"].NotNull_Or(RpcError.InvalidParams.WithData($"Invalid 'scopes' in Signer."));
        var contracts = obj["allowedcontracts"];
        var groups = obj["allowedgroups"];
        var rules = obj["rules"];
        return new Signer
        {
            Account = account!.AsString().AddressToScriptHash(addressVersion),
            Scopes = Result.Ok_Or(() => Enum.Parse<WitnessScope>(scopes!.AsString()),
                RpcError.InvalidParams.WithData($"Invalid 'scopes' in Signer.")),
            AllowedContracts = contracts is null ? [] :
                Result.Ok_Or(() => ((JArray)contracts).Select(p => UInt160.Parse(p!.AsString())).ToArray(),
                RpcError.InvalidParams.WithData($"Invalid 'allowedcontracts' in Signer.")),
            AllowedGroups = groups is null ? [] :
                Result.Ok_Or(() => ((JArray)groups).Select(p => ECPoint.Parse(p!.AsString(), ECCurve.Secp256r1)).ToArray(),
                RpcError.InvalidParams.WithData($"Invalid 'allowedgroups' in Signer.")),
            Rules = rules is null ? [] :
                Result.Ok_Or(() => ((JArray)rules).Select(r => WitnessRule.FromJson((JObject)r!)).ToArray(),
                    RpcError.InvalidParams.WithData($"Invalid 'rules' in Signer.")),
        };
    }

    /// <summary>
    /// Create a Signer array from a JSON array.
    /// Each item in the JSON array should be a JSON object with the following properties:
    /// - "account": A hex-encoded UInt160 or a Base58Check address, required.
    /// - "scopes": A enum string representing the scopes(WitnessScope) of the signer, required.
    /// - "allowedcontracts": An array of hex-encoded UInt160, optional.
    /// - "allowedgroups": An array of hex-encoded ECPoint, optional.
    /// - "rules": An array of strings representing the rules(WitnessRule) of the signer, optional.
    /// </summary>
    /// <param name="json">The JSON array to create a Signer array from.</param>
    /// <param name="addressVersion">The address version to use for the signers.</param>
    /// <returns>A Signer array.</returns>
    /// <exception cref="RpcException">Thrown when the JSON array is invalid or max allowed witness exceeded.</exception>
    internal static Signer[] ToSigners(this JArray json, byte addressVersion)
    {
        if (json.Count > Transaction.MaxTransactionAttributes)
            throw new RpcException(RpcError.InvalidParams.WithData("Max allowed signers exceeded."));

        var signers = new Signer[json.Count];
        for (var i = 0; i < json.Count; i++)
        {
            if (json[i] is null || json[i] is not JObject obj)
                throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Signer at {i}."));

            signers[i] = obj.ToSigner(addressVersion);
        }

        // Validate format
        _ = signers.ToByteArray().AsSerializableArray<Signer>();
        return signers;
    }

    internal static Signer[] ToSigners(this Address[] accounts, WitnessScope scopes)
    {
        if (accounts.Length > Transaction.MaxTransactionAttributes)
            throw new RpcException(RpcError.InvalidParams.WithData("Max allowed signers exceeded."));

        return accounts.Select(u => new Signer { Account = u.ScriptHash, Scopes = scopes }).ToArray();
    }

    /// <summary>
    /// Create a Witness array from a JSON array.   
    /// Each item in the JSON array should be a JSON object with the following properties:
    /// - "invocation": A base64-encoded string representing the invocation script, optional.
    /// - "verification": A base64-encoded string representing the verification script, optional.
    /// </summary>
    /// <param name="json">The JSON array to create a Witness array from.</param>
    /// <returns>A Witness array.</returns>
    /// <exception cref="RpcException">Thrown when the JSON array is invalid or max allowed witness exceeded.</exception>
    private static Witness[] ToWitnesses(this JArray json)
    {
        if (json.Count > Transaction.MaxTransactionAttributes)
            throw new RpcException(RpcError.InvalidParams.WithData("Max allowed witness exceeded."));

        var witnesses = new List<Witness>(json.Count);
        for (var i = 0; i < json.Count; i++)
        {
            if (json[i] is null || json[i] is not JObject obj)
                throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Witness at {i}."));

            var invocation = obj["invocation"];
            var verification = obj["verification"];
            if (invocation is null && verification is null) continue; // Keep same as before

            witnesses.Add(new Witness
            {
                InvocationScript = Convert.FromBase64String(invocation?.AsString() ?? string.Empty),
                VerificationScript = Convert.FromBase64String(verification?.AsString() ?? string.Empty)
            });
        }

        return witnesses.ToArray();
    }

    /// <summary>
    /// Converts an hex-encoded UInt160 or a Base58Check address to a script hash.
    /// </summary>
    /// <param name="address">The address to convert.</param>
    /// <param name="version">The address version to use for the conversion.</param>
    /// <returns>The script hash corresponding to the address.</returns>
    internal static UInt160 AddressToScriptHash(this string address, byte version)
    {
        if (UInt160.TryParse(address, out var scriptHash))
            return scriptHash;
        return Result.Ok_Or(() => address.ToScriptHash(version),
            RpcError.InvalidParams.WithData($"Invalid Address: {address}"));
    }

    internal static Address ToAddress(this JToken token, byte version)
    {
        if (token is null || token is not JString value)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Address: {token}"));

        var scriptHash = value.Value.AddressToScriptHash(version);
        return new Address(scriptHash, version);
    }

    internal static Address[] ToAddresses(this JToken token, byte version)
    {
        if (token is not JArray array)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Addresses: {token}"));

        var addresses = new Address[array.Count];
        for (var i = 0; i < array.Count; i++)
        {
            var item = array[i].NotNull_Or(RpcError.InvalidParams.WithData($"Invalid Address at {i}."));
            addresses[i] = item.ToAddress(version);
        }
        return addresses;
    }

    private static ContractParameter[] ToContractParameters(this JToken token)
    {
        if (token is not JArray array)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Addresses: {token}"));

        var parameters = new ContractParameter[array.Count];
        for (var i = 0; i < array.Count; i++)
        {
            if (array[i] is null || array[i] is not JObject obj)
                throw new RpcException(RpcError.InvalidParams.WithData($"Invalid ContractParameter at [{i}]"));
            parameters[i] = ContractParameter.FromJson(obj);
        }
        return parameters;
    }

    private static object ToGuid(JToken token)
    {
        if (token is null || token is not JString value)
            throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Guid: {token}"));

        if (Guid.TryParse(value.Value, out var guid)) return guid;

        throw new RpcException(RpcError.InvalidParams.WithData($"Invalid Guid: {token}"));
    }
}

public static class TypeExtensions
{
    public static bool IsFloatingPoint(this Type type)
    {
        return type == typeof(float) || type == typeof(double) || type == typeof(decimal);
    }
}
