// Copyright (C) 2015-2026 The Neo Project.
//
// ContractManifest.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.Extensions;
using Neo.Json;
using Neo.VM;
using Neo.VM.Types;
using System;
using System.Linq;
using Array = Neo.VM.Types.Array;

namespace Neo.SmartContract.Manifest
{
    /// <summary>
    /// Represents the manifest of a smart contract.
    /// When a smart contract is deployed, it must explicitly declare the features and permissions it will use.
    /// When it is running, it will be limited by its declared list of features and permissions, and cannot make any behavior beyond the scope of the list.
    /// </summary>
    /// <remarks>For more details, see NEP-15.</remarks>
    public class ContractManifest : IInteroperable
    {
        /// <summary>
        /// The maximum length of a manifest.
        /// </summary>
        public const int MaxLength = ushort.MaxValue;

        /// <summary>
        /// The name of the contract.
        /// </summary>
        public required string Name { get; set; }

        /// <summary>
        /// The groups of the contract.
        /// </summary>
        public required ContractGroup[] Groups { get; set; }

        /// <summary>
        /// Indicates which standards the contract supports. It can be a list of NEPs.
        /// </summary>
        public required string[] SupportedStandards { get; set; }

        /// <summary>
        /// The ABI of the contract.
        /// </summary>
        public required ContractAbi Abi { get; set; }

        /// <summary>
        /// The permissions of the contract.
        /// </summary>
        public required ContractPermission[] Permissions { get; set; }

        /// <summary>
        /// The trusted contracts and groups of the contract.
        /// If a contract is trusted, the user interface will not give any warnings when called by the contract.
        /// </summary>
        public required WildcardContainer<ContractPermissionDescriptor> Trusts { get; set; }

        /// <summary>
        /// Custom user data.
        /// </summary>
        public JObject? Extra { get; set; }

        void IInteroperable.FromStackItem(StackItem stackItem)
        {
            Struct @struct = (Struct)stackItem;
            Name = @struct[0].GetString()!;
            Groups = ((Array)@struct[1]).Select(p => p.ToInteroperable<ContractGroup>()).ToArray();
            if (((Map)@struct[2]).Count != 0)
                throw new ArgumentException("Features field must be empty", nameof(stackItem));

            SupportedStandards = ((Array)@struct[3]).Select(p => p.GetString()!).ToArray();
            Abi = @struct[4].ToInteroperable<ContractAbi>();
            Permissions = ((Array)@struct[5]).Select(p => p.ToInteroperable<ContractPermission>()).ToArray();
            Trusts = @struct[6] switch
            {
                Null _ => WildcardContainer<ContractPermissionDescriptor>.CreateWildcard(),
                // Array array when array.Any(p => ((ByteString)p).Size == 0) => WildcardContainer<ContractPermissionDescriptor>.CreateWildcard(),
                Array array => WildcardContainer<ContractPermissionDescriptor>.Create(array.Select(ContractPermissionDescriptor.Create).ToArray()),
                _ => throw new ArgumentException("Trusts field must be null or array", nameof(stackItem))
            };
            Extra = (JObject?)JToken.Parse(@struct[7].GetSpan());
        }

        public StackItem ToStackItem(IReferenceCounter? referenceCounter)
        {
            return new Struct(referenceCounter)
            {
                Name,
                new Array(referenceCounter, Groups.Select(p => p.ToStackItem(referenceCounter))),
                new Map(referenceCounter),
                new Array(referenceCounter, SupportedStandards.Select(p => (StackItem)p)),
                Abi.ToStackItem(referenceCounter),
                new Array(referenceCounter, Permissions.Select(p => p.ToStackItem(referenceCounter))),
                Trusts.IsWildcard ? StackItem.Null : new Array(referenceCounter, Trusts.Select(p => p.ToArray()?? StackItem.Null)),
                Extra is null ? "null" : Extra.ToByteArray(false)
            };
        }

        /// <summary>
        /// Converts the manifest from a JSON object.
        /// </summary>
        /// <param name="json">The manifest represented by a JSON object.</param>
        /// <returns>The converted manifest.</returns>
        public static ContractManifest FromJson(JObject json)
        {
            ContractManifest manifest = new()
            {
                Name = json["name"]!.GetString(),
                Groups = ((JArray?)json["groups"])?.Select(u => ContractGroup.FromJson((JObject)u!)).ToArray() ?? [],
                SupportedStandards = ((JArray?)json["supportedstandards"])?.Select(u => u!.GetString()).ToArray() ?? [],
                Abi = ContractAbi.FromJson((JObject)json["abi"]!),
                Permissions = ((JArray?)json["permissions"])?.Select(u => ContractPermission.FromJson((JObject)u!)).ToArray() ?? [],
                Trusts = WildcardContainer<ContractPermissionDescriptor>.FromJson(json["trusts"]!, u => ContractPermissionDescriptor.FromJson((JString)u)),
                Extra = (JObject?)json["extra"]
            };

            if (string.IsNullOrEmpty(manifest.Name))
                throw new FormatException("Name in ContractManifest is empty");
            _ = manifest.Groups.ToDictionary(p => p.PubKey);
            if (json["features"] is not JObject features || features.Count != 0)
                throw new FormatException("Features field must be empty");
            if (manifest.SupportedStandards.Any(string.IsNullOrEmpty))
                throw new FormatException("SupportedStandards in ContractManifest has empty string");
            _ = manifest.SupportedStandards.ToDictionary(p => p);
            _ = manifest.Permissions.ToDictionary(p => p.Contract);
            _ = manifest.Trusts.ToDictionary(p => p);
            return manifest;
        }

        /// <summary>
        /// Parse the manifest from a byte array containing JSON data.
        /// </summary>
        /// <param name="json">The byte array containing JSON data.</param>
        /// <returns>The parsed manifest.</returns>
        public static ContractManifest Parse(ReadOnlySpan<byte> json)
        {
            if (json.Length > MaxLength)
                throw new ArgumentException($"JSON content length {json.Length} exceeds maximum allowed size of {MaxLength} bytes", nameof(json));
            return FromJson((JObject)JToken.Parse(json)!);
        }

        /// <summary>
        /// Parse the manifest from a JSON <see cref="string"/>.
        /// </summary>
        /// <param name="json">The JSON <see cref="string"/>.</param>
        /// <returns>The parsed manifest.</returns>
        public static ContractManifest Parse(string json) => Parse(json.ToStrictUtf8Bytes());

        /// <summary>
        /// Converts the manifest to a JSON object.
        /// </summary>
        /// <returns>The manifest represented by a JSON object.</returns>
        public JObject ToJson()
        {
            return new JObject
            {
                ["name"] = Name,
                ["groups"] = Groups.Select(u => u.ToJson()).ToArray(),
                ["features"] = new JObject(),
                ["supportedstandards"] = SupportedStandards.Select(u => new JString(u)).ToArray(),
                ["abi"] = Abi.ToJson(),
                ["permissions"] = Permissions.Select(p => p.ToJson()).ToArray(),
                ["trusts"] = Trusts.ToJson(p => p.ToJson()),
                ["extra"] = Extra
            };
        }

        /// <summary>
        /// Determines whether the manifest is valid.
        /// </summary>
        /// <param name="limits">The <see cref="ExecutionEngineLimits"/> used for test serialization.</param>
        /// <param name="hash">The hash of the contract.</param>
        /// <returns><see langword="true"/> if the manifest is valid; otherwise, <see langword="false"/>.</returns>
        public bool IsValid(ExecutionEngineLimits limits, UInt160 hash)
        {
            // Ensure that is serializable
            try
            {
                _ = BinarySerializer.Serialize(ToStackItem(null), limits);
            }
            catch
            {
                return false;
            }
            // Check groups
            return Groups.All(u => u.IsValid(hash));
        }
    }
}
