package native

import (
	"cmp"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"maps"
	"math/big"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/bigint"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

const (
	defaultExecFeeFactor      = interop.DefaultBaseExecFee
	defaultFeePerByte         = 1000
	defaultMaxVerificationGas = 1_50000000
	// defaultAttributeFee is a default fee for a transaction attribute those price wasn't set yet.
	defaultAttributeFee = 0
	// defaultNotaryAssistedFee is a default fee for a NotaryAssisted transaction attribute per key.
	defaultNotaryAssistedFee = 1000_0000 // 0.1 GAS
	// DefaultStoragePrice is the price to pay for 1 byte of storage.
	DefaultStoragePrice = 100000

	// maxExecFeeFactor is the maximum allowed execution fee factor.
	maxExecFeeFactor uint32 = 100
	// maxFeePerByte is the maximum allowed fee per byte value.
	maxFeePerByte = 100_000_000
	// maxStoragePrice is the maximum allowed price for a byte of storage.
	maxStoragePrice = 10000000
	// maxAttributeFee is the maximum allowed value for a transaction attribute fee.
	maxAttributeFee = 10_00000000
	// maxMillisecondsPerBlock is the maximum allowed value (in milliseconds) for a block generation time.
	maxMillisecondsPerBlock = 30_000
	// maxMaxVUBIncrement the maximum value for upper increment size of blockchain
	// height (in blocks) exceeding that a transaction should fail validation.
	maxMaxVUBIncrement = 86400
	// the maximum value of maximum number of blocks reachable to contracts.
	maxMaxTraceableBlocks = 2102400

	// blockedAccountPrefix is a prefix used to store blocked account.
	blockedAccountPrefix = 15
	// whitelistedFeeContractPrefix is a prefix used to store whitelisted contract.
	whitelistedFeeContractPrefix = 16
	// attributeFeePrefix is a prefix used to store attribute fee.
	attributeFeePrefix = 20
)

var (
	// execFeeFactorKey is a key used to store execution fee factor.
	execFeeFactorKey = []byte{18}
	// feePerByteKey is a key used to store the minimum fee per byte for
	// transaction.
	feePerByteKey = []byte{10}
	// storagePriceKey is a key used to store storage price.
	storagePriceKey = []byte{19}
	// msPerBlockKey is a key used to store block generation time.
	msPerBlockKey = []byte{21}
	// maxVUBIncrementKey is a key used to store maximum ValidUntilBlock increment.
	maxVUBIncrementKey = []byte{22}
	// MaxTraceableBlocksKey is a key used to store the maximum number of traceable blocks.
	MaxTraceableBlocksKey = []byte{23}
)

// Policy represents Policy native contract.
type Policy struct {
	interop.ContractMD
	Tutus ITutus
}

type PolicyCache struct {
	execFeeFactor        uint32
	faunInitialized      bool
	feePerByte           int64
	maxVerificationGas   int64
	storagePrice         uint32
	msPerBlock           uint32
	maxVUBIncrement      uint32
	maxTraceableBlocks   uint32
	attributeFee         map[transaction.AttrType]uint32
	blockedAccounts      []util.Uint160
	whitelistedContracts []whitelistedContract
}

// whitelistedContract is a structure representing a whitelisted contract with
// the pre-defined execution price.
type whitelistedContract struct {
	hash   util.Uint160
	offset uint32
	fee    int64
}

var _ = (stackitem.Convertible)(&whitelistedContract{})

// Compare compares two whitelistedContract structures in the order that matches
// a whitelisted contract key serialization format.
func (c whitelistedContract) Compare(other whitelistedContract) int {
	return cmp.Or(
		c.hash.Compare(other.hash),
		cmp.Compare(c.offset, other.offset), // offset is stored in BE.
	)
}

// ToStackItem implements [stackitem.Convertible] interface.
func (c whitelistedContract) ToStackItem() (stackitem.Item, error) {
	return stackitem.NewByteArray(makeWhitelistedKey(c.hash, c.offset)[1:]), // strip prefix.
		nil
}

// FromStackItem implements [stackitem.Convertible] interface. Not really needed
// since whitelistedContract conversion is one-directional only.
func (c *whitelistedContract) FromStackItem(item stackitem.Item) error {
	panic("not supported")
}

var (
	_ interop.Contract        = (*Policy)(nil)
	_ dao.NativeContractCache = (*PolicyCache)(nil)
)

// Copy implements NativeContractCache interface.
func (c *PolicyCache) Copy() dao.NativeContractCache {
	cp := &PolicyCache{}
	copyPolicyCache(c, cp)
	return cp
}

func copyPolicyCache(src, dst *PolicyCache) {
	*dst = *src
	dst.attributeFee = maps.Clone(src.attributeFee)
	dst.blockedAccounts = slices.Clone(src.blockedAccounts)
	dst.whitelistedContracts = slices.Clone(src.whitelistedContracts)
}

// newPolicy returns Policy native contract.
func newPolicy() *Policy {
	p := &Policy{ContractMD: *interop.NewContractMD(nativenames.Policy, nativeids.PolicyContract)}
	defer p.BuildHFSpecificMD(p.ActiveIn())

	desc := NewDescriptor("getFeePerByte", smartcontract.IntegerType)
	md := NewMethodAndPrice(p.getFeePerByte, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("isBlocked", smartcontract.BoolType,
		manifest.NewParameter("account", smartcontract.Hash160Type))
	md = NewMethodAndPrice(p.isBlocked, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getExecFeeFactor", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getExecFeeFactor, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setExecFeeFactor", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setExecFeeFactor, 1<<15, callflag.States)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getStoragePrice", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getStoragePrice, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setStoragePrice", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setStoragePrice, 1<<15, callflag.States)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getAttributeFee", smartcontract.IntegerType,
		manifest.NewParameter("attributeType", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getAttributeFeeV0, 1<<15, callflag.ReadStates, config.HFDefault, transaction.NotaryAssistedActivation)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getAttributeFee", smartcontract.IntegerType,
		manifest.NewParameter("attributeType", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getAttributeFeeV1, 1<<15, callflag.ReadStates, transaction.NotaryAssistedActivation)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setAttributeFee", smartcontract.VoidType,
		manifest.NewParameter("attributeType", smartcontract.IntegerType),
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setAttributeFeeV0, 1<<15, callflag.States, config.HFDefault, transaction.NotaryAssistedActivation)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setAttributeFee", smartcontract.VoidType,
		manifest.NewParameter("attributeType", smartcontract.IntegerType),
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setAttributeFeeV1, 1<<15, callflag.States, transaction.NotaryAssistedActivation)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setFeePerByte", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setFeePerByte, 1<<15, callflag.States)
	p.AddMethod(md, desc)

	desc = NewDescriptor("blockAccount", smartcontract.BoolType,
		manifest.NewParameter("account", smartcontract.Hash160Type))
	md = NewMethodAndPrice(p.blockAccount, 1<<15, callflag.States, config.HFDefault, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("blockAccount", smartcontract.BoolType,
		manifest.NewParameter("account", smartcontract.Hash160Type))
	md = NewMethodAndPrice(p.blockAccount, 1<<15, callflag.States|callflag.AllowNotify, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("unblockAccount", smartcontract.BoolType,
		manifest.NewParameter("account", smartcontract.Hash160Type))
	md = NewMethodAndPrice(p.unblockAccount, 1<<15, callflag.States)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getMaxValidUntilBlockIncrement", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getMaxValidUntilBlockIncrement, 1<<15, callflag.ReadStates, config.HFEchidna)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setMaxValidUntilBlockIncrement", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setMaxValidUntilBlockIncrement, 1<<15, callflag.States, config.HFEchidna)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getMillisecondsPerBlock", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getMillisecondsPerBlock, 1<<15, callflag.ReadStates, config.HFEchidna)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setMillisecondsPerBlock", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setMillisecondsPerBlock, 1<<15, callflag.States|callflag.AllowNotify, config.HFEchidna)
	p.AddMethod(md, desc)

	eDesc := NewEventDescriptor("MillisecondsPerBlockChanged",
		manifest.NewParameter("old", smartcontract.IntegerType),
		manifest.NewParameter("new", smartcontract.IntegerType),
	)
	eMD := NewEvent(eDesc, config.HFEchidna)
	p.AddEvent(eMD)

	desc = NewDescriptor("getMaxTraceableBlocks", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getMaxTraceableBlocks, 1<<15, callflag.ReadStates, config.HFEchidna)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setMaxTraceableBlocks", smartcontract.VoidType,
		manifest.NewParameter("value", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setMaxTraceableBlocks, 1<<15, callflag.States, config.HFEchidna)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getBlockedAccounts", smartcontract.InteropInterfaceType)
	md = NewMethodAndPrice(p.getBlockedAccounts, 1<<15, callflag.ReadStates, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getExecPicoFeeFactor", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getExecPicoFeeFactor, 1<<15, callflag.ReadStates, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("setWhitelistFeeContract", smartcontract.VoidType,
		manifest.NewParameter("contractHash", smartcontract.Hash160Type),
		manifest.NewParameter("method", smartcontract.StringType),
		manifest.NewParameter("argCount", smartcontract.IntegerType),
		manifest.NewParameter("fixedFee", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.setWhitelistFeeContract, 1<<15, callflag.States|callflag.AllowNotify, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("removeWhitelistFeeContract", smartcontract.VoidType,
		manifest.NewParameter("contractHash", smartcontract.Hash160Type),
		manifest.NewParameter("method", smartcontract.StringType),
		manifest.NewParameter("argCount", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.removeWhitelistFeeContract, 1<<15, callflag.States|callflag.AllowNotify, config.HFFaun)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getWhitelistFeeContracts", smartcontract.InteropInterfaceType)
	md = NewMethodAndPrice(p.getWhitelistFeeContracts, 1<<15, callflag.ReadStates, config.HFFaun)
	p.AddMethod(md, desc)

	eDesc = NewEventDescriptor("WhitelistFeeChanged",
		manifest.NewParameter("contract", smartcontract.Hash160Type),
		manifest.NewParameter("method", smartcontract.StringType),
		manifest.NewParameter("argCount", smartcontract.IntegerType),
		manifest.NewParameter("fee", smartcontract.AnyType),
	)
	eMD = NewEvent(eDesc, config.HFFaun)
	p.AddEvent(eMD)

	return p
}

// Metadata implements the Contract interface.
func (p *Policy) Metadata() *interop.ContractMD {
	return &p.ContractMD
}

// Initialize initializes Policy native contract and implements the Contract interface.
func (p *Policy) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
	if hf == p.ActiveIn() {
		setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte)
		setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor)
		setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)

		cache := &PolicyCache{
			execFeeFactor:        defaultExecFeeFactor,
			feePerByte:           defaultFeePerByte,
			maxVerificationGas:   defaultMaxVerificationGas,
			storagePrice:         DefaultStoragePrice,
			attributeFee:         map[transaction.AttrType]uint32{},
			blockedAccounts:      make([]util.Uint160, 0),
			whitelistedContracts: make([]whitelistedContract, 0),
		}
		ic.DAO.SetCache(p.ID, cache)
	}

	if hf != nil && *hf == config.HFEchidna {
		cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)

		maxVUBIncrement := ic.Chain.GetConfig().Genesis.MaxValidUntilBlockIncrement
		setIntWithKey(p.ID, ic.DAO, maxVUBIncrementKey, int64(maxVUBIncrement))
		cache.maxVUBIncrement = maxVUBIncrement

		msPerBlock := ic.Chain.GetConfig().Genesis.TimePerBlock.Milliseconds()
		setIntWithKey(p.ID, ic.DAO, msPerBlockKey, msPerBlock)
		cache.msPerBlock = uint32(msPerBlock)

		maxTraceableBlocks := ic.Chain.GetConfig().Genesis.MaxTraceableBlocks
		setIntWithKey(p.ID, ic.DAO, MaxTraceableBlocksKey, int64(maxTraceableBlocks))
		cache.maxTraceableBlocks = maxTraceableBlocks

		setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(transaction.NotaryAssistedT)}, defaultNotaryAssistedFee)
		cache.attributeFee[transaction.NotaryAssistedT] = defaultNotaryAssistedFee
	}

	if hf != nil && *hf == config.HFFaun {
		cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
		cache.execFeeFactor = cache.execFeeFactor * vm.ExecFeeFactorMultiplier
		cache.faunInitialized = true
		setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(cache.execFeeFactor))
	}

	return nil
}

func (p *Policy) InitializeCache(isHardforkEnabled interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	cache := &PolicyCache{}
	err := p.fillCacheFromDAO(cache, d, isHardforkEnabled, blockHeight)
	if err != nil {
		return err
	}
	d.SetCache(p.ID, cache)
	return nil
}

func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple, isHardforkEnabled interop.IsHardforkEnabled, blockHeight uint32) error {
	cache.execFeeFactor = uint32(getIntWithKey(p.ID, d, execFeeFactorKey))
	cache.feePerByte = getIntWithKey(p.ID, d, feePerByteKey)
	cache.maxVerificationGas = defaultMaxVerificationGas
	cache.storagePrice = uint32(getIntWithKey(p.ID, d, storagePriceKey))

	cache.blockedAccounts = make([]util.Uint160, 0)
	var fErr error
	d.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool {
		hash, err := util.Uint160DecodeBytesBE(k)
		if err != nil {
			fErr = fmt.Errorf("failed to decode blocked account hash: %w", err)
			return false
		}
		cache.blockedAccounts = append(cache.blockedAccounts, hash)
		return true
	})
	if fErr != nil {
		return fmt.Errorf("failed to initialize blocked accounts: %w", fErr)
	}

	cache.attributeFee = make(map[transaction.AttrType]uint32)
	d.Seek(p.ID, storage.SeekRange{Prefix: []byte{attributeFeePrefix}}, func(k, v []byte) bool {
		if len(k) != 1 {
			fErr = fmt.Errorf("unexpected attribute type len %d (%s)", len(k), hex.EncodeToString(k))
			return false
		}
		t := transaction.AttrType(k[0])
		value := bigint.FromBytes(v)
		if value == nil {
			fErr = fmt.Errorf("unexpected attribute value format: key=%s, value=%s", hex.EncodeToString(k), hex.EncodeToString(v))
			return false
		}
		cache.attributeFee[t] = uint32(value.Int64())
		return true
	})
	if fErr != nil {
		return fmt.Errorf("failed to initialize attribute fees: %w", fErr)
	}

	var echidna = config.HFEchidna
	if isHardforkEnabled(&echidna, blockHeight) {
		cache.maxVUBIncrement = uint32(getIntWithKey(p.ID, d, maxVUBIncrementKey))
		cache.msPerBlock = uint32(getIntWithKey(p.ID, d, msPerBlockKey))
		cache.maxTraceableBlocks = uint32(getIntWithKey(p.ID, d, MaxTraceableBlocksKey))
	}

	cache.whitelistedContracts = make([]whitelistedContract, 0)
	var faun = config.HFFaun
	if isHardforkEnabled(&faun, blockHeight) {
		cache.faunInitialized = true
		d.Seek(p.ID, storage.SeekRange{Prefix: []byte{whitelistedFeeContractPrefix}}, func(k, v []byte) bool {
			if len(k) != util.Uint160Size+4 {
				fErr = fmt.Errorf("unexpected whitelisted contract key length %d vs %d", len(k), util.Uint160Size+4)
				return false
			}
			h, err := util.Uint160DecodeBytesBE(k[:util.Uint160Size])
			if err != nil {
				fErr = fmt.Errorf("failed to decode whitelisted contract hash: %w", err)
				return false
			}
			offset := binary.BigEndian.Uint32(k[util.Uint160Size:])
			value := bigint.FromBytes(v)
			if value == nil {
				fErr = fmt.Errorf("unexpected whitelisted contract fee format: key=%s, value=%s", hex.EncodeToString(k), hex.EncodeToString(v))
				return false
			}
			cache.whitelistedContracts = append(cache.whitelistedContracts, whitelistedContract{
				hash:   h,
				offset: offset,
				fee:    value.Int64(),
			})
			return true
		})
		if fErr != nil {
			return fmt.Errorf("failed to initialize whitelisted contracts: %w", fErr)
		}
	}

	return nil
}

// OnPersist implements Contract interface.
func (p *Policy) OnPersist(ic *interop.Context) error {
	return nil
}

// PostPersist implements Contract interface.
func (p *Policy) PostPersist(ic *interop.Context) error {
	return nil
}

// ActiveIn implements the Contract interface.
func (p *Policy) ActiveIn() *config.Hardfork {
	return nil
}

// getFeePerByte is a Policy contract method that returns the required transaction's fee
// per byte.
func (p *Policy) getFeePerByte(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(p.GetFeePerByteInternal(ic.DAO)))
}

// GetFeePerByteInternal returns required transaction's fee per byte.
func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 {
	cache := dao.GetROCache(p.ID).(*PolicyCache)
	return cache.feePerByte
}

// GetMaxVerificationGas returns the maximum gas allowed to be burned during verification.
func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 {
	cache := dao.GetROCache(p.ID).(*PolicyCache)
	return cache.maxVerificationGas
}

// getExecFeeFactor returns the current execution fee factor in Datoshi units.
func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(p.GetExecFeeFactorInternal(ic.DAO) / vm.ExecFeeFactorMultiplier))
}

// GetExecFeeFactorInternal returns the current execution fee factor in picoGAS
// units.
func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	if cache.faunInitialized {
		return int64(cache.execFeeFactor)
	}
	return int64(cache.execFeeFactor * vm.ExecFeeFactorMultiplier)
}

// getExecPicoFeeFactor is active starting from [config.HFFaun] and returns the
// current execution fee factor in picoGAS units.
func (p *Policy) getExecPicoFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(p.ID).(*PolicyCache)
	return stackitem.NewBigInteger(big.NewInt(int64(cache.execFeeFactor)))
}

func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toUint32(args[0])
	maxValue := maxExecFeeFactor
	if ic.IsHardforkEnabled(config.HFFaun) {
		maxValue *= vm.ExecFeeFactorMultiplier
	}
	if value <= 0 || maxValue < value {
		panic(fmt.Errorf("ExecFeeFactor must be between 1 and %d", maxExecFeeFactor))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.execFeeFactor = value
	return stackitem.Null{}
}

// isBlocked is Policy contract method that checks whether provided account is blocked.
func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	hash := toUint160(args[0])
	_, blocked := p.isBlockedInternal(ic.DAO.GetROCache(p.ID).(*PolicyCache), hash)
	return stackitem.NewBool(blocked)
}

// IsBlocked checks whether provided account is blocked. Normally it uses Policy
// cache, falling back to the DB queries when Policy cache is not available yet
// (the only case is native cache initialization pipeline, where native Neo cache
// is being initialized before the Policy's one).
func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool {
	cache := dao.GetROCache(p.ID)
	if cache == nil {
		key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
		return dao.GetStorageItem(p.ID, key) != nil
	}
	_, isBlocked := p.isBlockedInternal(cache.(*PolicyCache), hash)
	return isBlocked
}

// isBlockedInternal checks whether provided account is blocked. It returns position
// of the blocked account in the blocked accounts list (or the position it should be
// put at).
func (p *Policy) isBlockedInternal(roCache *PolicyCache, hash util.Uint160) (int, bool) {
	return slices.BinarySearchFunc(roCache.blockedAccounts, hash, util.Uint160.Compare)
}

func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(p.GetStoragePriceInternal(ic.DAO) / vm.ExecFeeFactorMultiplier))
}

// GetStoragePriceInternal returns the current storage price in picoGAS units.
func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	return int64(cache.storagePrice) * vm.ExecFeeFactorMultiplier
}

func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toUint32(args[0])
	if value <= 0 || maxStoragePrice < value {
		panic(fmt.Errorf("StoragePrice must be between 1 and %d", maxStoragePrice))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.storagePrice = value
	return stackitem.Null{}
}

func (p *Policy) getAttributeFeeV0(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return p.getAttributeFeeGeneric(ic, args, false)
}

func (p *Policy) getAttributeFeeV1(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return p.getAttributeFeeGeneric(ic, args, true)
}

func (p *Policy) getAttributeFeeGeneric(ic *interop.Context, args []stackitem.Item, allowNotaryAssisted bool) stackitem.Item {
	t := transaction.AttrType(toUint8(args[0]))
	if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) ||
		(!allowNotaryAssisted && t == transaction.NotaryAssistedT) {
		panic(fmt.Errorf("invalid attribute type: %d", t))
	}
	return stackitem.NewBigInteger(big.NewInt(p.GetAttributeFeeInternal(ic.DAO, t)))
}

func (p *Policy) getBlockedAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(p.ID).(*PolicyCache)
	cloned := slices.Clone(cache.blockedAccounts)
	return stackitem.NewInterop(&iterator[util.Uint160]{keys: cloned})
}

// GetAttributeFeeInternal returns required transaction's attribute fee.
func (p *Policy) GetAttributeFeeInternal(d *dao.Simple, t transaction.AttrType) int64 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	v, ok := cache.attributeFee[t]
	if !ok {
		// We may safely omit this part, but let it be here in case if defaultAttributeFee value is changed.
		v = defaultAttributeFee
	}
	return int64(v)
}

func (p *Policy) setAttributeFeeV0(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return p.setAttributeFeeGeneric(ic, args, false)
}

func (p *Policy) setAttributeFeeV1(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return p.setAttributeFeeGeneric(ic, args, true)
}

func (p *Policy) setAttributeFeeGeneric(ic *interop.Context, args []stackitem.Item, allowNotaryAssisted bool) stackitem.Item {
	t := transaction.AttrType(toUint8(args[0]))
	value := toUint32(args[1])
	if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) ||
		(!allowNotaryAssisted && t == transaction.NotaryAssistedT) {
		panic(fmt.Errorf("invalid attribute type: %d", t))
	}
	if value > maxAttributeFee {
		panic(fmt.Errorf("attribute value is out of range: %d", value))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(t)}, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.attributeFee[t] = value
	return stackitem.Null{}
}

// setFeePerByte is a Policy contract method that sets transaction's fee per byte.
func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toBigInt(args[0]).Int64()
	if value < 0 || value > maxFeePerByte {
		panic(fmt.Errorf("FeePerByte shouldn't be negative or greater than %d", maxFeePerByte))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, feePerByteKey, value)
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.feePerByte = value
	return stackitem.Null{}
}

// blockAccount is a Policy contract method that adds the given account hash to the list
// of blocked accounts.
func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	hash := toUint160(args[0])
	for i := range ic.Natives {
		if ic.Natives[i].Metadata().Hash == hash {
			panic("cannot block native contract")
		}
	}
	return stackitem.NewBool(p.BlockAccountInternal(ic, hash))
}

func (p *Policy) BlockAccountInternal(ic *interop.Context, hash util.Uint160) bool {
	i, blocked := p.isBlockedInternal(ic.DAO.GetROCache(p.ID).(*PolicyCache), hash)
	if blocked {
		return false
	}
	var _ = p.Tutus.RevokeVotes(ic, hash) // ignore error, as in the reference.
	key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
	ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{})
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	if len(cache.blockedAccounts) == i {
		cache.blockedAccounts = append(cache.blockedAccounts, hash)
	} else {
		cache.blockedAccounts = append(cache.blockedAccounts[:i+1], cache.blockedAccounts[i:]...)
		cache.blockedAccounts[i] = hash
	}
	return true
}

// unblockAccount is a Policy contract method that removes the given account hash from
// the list of blocked accounts.
func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	hash := toUint160(args[0])
	i, blocked := p.isBlockedInternal(ic.DAO.GetROCache(p.ID).(*PolicyCache), hash)
	if !blocked {
		return stackitem.NewBool(false)
	}
	key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
	ic.DAO.DeleteStorageItem(p.ID, key)
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...)
	return stackitem.NewBool(true)
}

func (p *Policy) getMaxValidUntilBlockIncrement(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(int64(p.GetMaxValidUntilBlockIncrementFromCache(ic.DAO))))
}

// GetMaxValidUntilBlockIncrementInternal returns current MaxValidUntilBlockIncrement.
// It respects Echidna enabling height.
func (p *Policy) GetMaxValidUntilBlockIncrementInternal(ic *interop.Context) uint32 {
	if ic.IsHardforkEnabled(config.HFEchidna) {
		return p.GetMaxValidUntilBlockIncrementFromCache(ic.DAO)
	}
	return ic.Chain.GetConfig().MaxValidUntilBlockIncrement
}

// GetMaxValidUntilBlockIncrementFromCache returns current MaxValidUntilBlockIncrement.
// It doesn't check neither Echidna enabling height nor cache initialization, so it's
// the caller's duty to ensure that Echidna is enabled before a call to this method.
func (p *Policy) GetMaxValidUntilBlockIncrementFromCache(d *dao.Simple) uint32 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	return cache.maxVUBIncrement
}

func (p *Policy) setMaxValidUntilBlockIncrement(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toUint32(args[0])
	if value <= 0 || maxMaxVUBIncrement < value {
		panic(fmt.Errorf("MaxValidUntilBlockIncrement should be positive and not greater than %d, got %d", maxMaxVUBIncrement, value))
	}
	mtb := p.GetMaxTraceableBlocksInternal(ic.DAO)
	if value >= mtb {
		panic(fmt.Errorf("MaxValidUntilBlockIncrement should be less than MaxTraceableBlocks %d, got %d", mtb, value))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, maxVUBIncrementKey, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.maxVUBIncrement = value

	return stackitem.Null{}
}

func (p *Policy) getMillisecondsPerBlock(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(int64(p.GetMillisecondsPerBlockInternal(ic.DAO))))
}

// GetMillisecondsPerBlockInternal returns current block generation time in milliseconds.
func (p *Policy) GetMillisecondsPerBlockInternal(d *dao.Simple) uint32 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	return cache.msPerBlock
}

func (p *Policy) setMillisecondsPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toUint32(args[0])
	if value <= 0 || maxMillisecondsPerBlock < value {
		panic(fmt.Errorf("MillisecondsPerBlock should be positive and not greater than %d, got %d", maxMillisecondsPerBlock, value))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	setIntWithKey(p.ID, ic.DAO, msPerBlockKey, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	old := cache.msPerBlock
	cache.msPerBlock = value

	err := ic.AddNotification(p.Hash, "MillisecondsPerBlockChanged", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(old))),
		stackitem.NewBigInteger(big.NewInt(int64(value))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.Null{}
}

func (p *Policy) getMaxTraceableBlocks(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(int64(p.GetMaxTraceableBlocksInternal(ic.DAO))))
}

// GetMaxTraceableBlocksInternal returns current MaxValidUntilBlockIncrement.
func (p *Policy) GetMaxTraceableBlocksInternal(d *dao.Simple) uint32 {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	return cache.maxTraceableBlocks
}

func (p *Policy) setMaxTraceableBlocks(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	value := toUint32(args[0])
	if value <= 0 || maxMaxTraceableBlocks < value {
		panic(fmt.Errorf("MaxTraceableBlocks should be positive and not greater than %d, got %d", maxMaxTraceableBlocks, value))
	}
	old := p.GetMaxTraceableBlocksInternal(ic.DAO)
	if value > old {
		panic(fmt.Errorf("MaxTraceableBlocks should not be greater than previous value %d, got %d", old, value))
	}
	maxVUBInc := p.GetMaxValidUntilBlockIncrementFromCache(ic.DAO)
	if value <= maxVUBInc {
		panic(fmt.Errorf("MaxTraceableBlocks should be larger than MaxValidUntilBlockIncrement %d, got %d", maxVUBInc, value))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}

	setIntWithKey(p.ID, ic.DAO, MaxTraceableBlocksKey, int64(value))
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	cache.maxTraceableBlocks = value

	return stackitem.Null{}
}

// CheckPolicy checks whether a transaction conforms to the current policy restrictions,
// like not being signed by a blocked account or not exceeding the block-level system
// fee limit.
func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error {
	cache := d.GetROCache(p.ID).(*PolicyCache)
	for _, signer := range tx.Signers {
		if _, isBlocked := p.isBlockedInternal(cache, signer.Account); isBlocked {
			return fmt.Errorf("account %s is blocked", signer.Account.StringLE())
		}
	}
	return nil
}

// WhitelistedFee checks whether the specified contract method is whitelisted and
// returns a non-negative execution fee in picoGAS units if so. It always uses
// native cache, hence cache is expected to be initialized by this moment.
func (p *Policy) WhitelistedFee(dao *dao.Simple, hash util.Uint160, offset int) int64 {
	cache := dao.GetROCache(p.ID).(*PolicyCache)
	i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, whitelistedContract{
		hash:   hash,
		offset: uint32(offset),
	}, whitelistedContract.Compare)
	if !ok {
		return -1
	}
	return cache.whitelistedContracts[i].fee * vm.ExecFeeFactorMultiplier
}

// CleanWhitelist removes a contract with the specified hash from the list of
// whitelisted contracts.
func (p *Policy) CleanWhitelist(ic *interop.Context, cs *state.Contract) error {
	var (
		cache = ic.DAO.GetRWCache(p.ID).(*PolicyCache)
		err   error
	)
	cache.whitelistedContracts = slices.DeleteFunc(cache.whitelistedContracts, func(c whitelistedContract) bool {
		if err != nil {
			return false
		}
		if !c.hash.Equals(cs.Hash) {
			return false
		}
		k := makeWhitelistedKey(c.hash, c.offset)
		ic.DAO.DeleteStorageItem(p.ID, k)
		m := cs.Manifest.ABI.GetMethodByOffset(int(c.offset))
		if m == nil {
			err = fmt.Errorf("method with offset %d not found in contract %s", c.offset, cs.Hash.StringLE())
			return false
		}
		err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{
			stackitem.NewByteArray(cs.Hash.BytesBE()),
			stackitem.NewByteArray([]byte(m.Name)),
			stackitem.NewBigInteger(big.NewInt(int64(len(m.Parameters)))),
			stackitem.Null{},
		}))

		return err == nil
	})

	return err
}

func (p *Policy) removeWhitelistFeeContract(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}
	h := toUint160(args[0])
	method := toString(args[1])
	argCnt := int(toInt64(args[2]))

	cs, err := ic.GetContract(h)
	if err != nil {
		panic(fmt.Errorf("failed to get contract %s: %w", h.StringLE(), err))
	}
	md := cs.Manifest.ABI.GetMethod(method, argCnt)
	if md == nil {
		panic(fmt.Errorf("method not found: %s/%d", method, argCnt))
	}

	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	k := makeWhitelistedKey(h, uint32(md.Offset))
	i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, whitelistedContract{
		hash:   h,
		offset: uint32(md.Offset),
	}, whitelistedContract.Compare)
	if !ok {
		panic(fmt.Errorf("whitelist for %s/%d not found", h.StringLE(), md.Offset))
	}
	ic.DAO.DeleteStorageItem(p.ID, k)
	cache.whitelistedContracts = slices.Delete(cache.whitelistedContracts, i, i+1)

	err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(h.BytesBE()),
		stackitem.NewByteArray([]byte(method)),
		stackitem.NewBigInteger(big.NewInt(int64(argCnt))),
		stackitem.Null{},
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.Null{}
}

func (p *Policy) setWhitelistFeeContract(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	h := toUint160(args[0])
	method := toString(args[1])
	argCnt := int(toInt64(args[2]))
	fee := toInt64(args[3])
	if fee < 0 {
		panic(fmt.Errorf("fee should be positive, got %d", fee))
	}
	if !p.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}

	cs, err := ic.GetContract(h)
	if err != nil {
		panic(fmt.Errorf("contract %s not found: %w", h.StringLE(), err))
	}
	md := cs.Manifest.ABI.GetMethod(method, argCnt)
	if md == nil {
		panic(fmt.Errorf("contract %s: method not found: %s/%d", h.StringLE(), method, argCnt))
	}

	setIntWithKey(p.ID, ic.DAO, makeWhitelistedKey(h, uint32(md.Offset)), fee)
	cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
	c := whitelistedContract{
		hash:   h,
		offset: uint32(md.Offset),
		fee:    fee,
	}
	i, ok := slices.BinarySearchFunc(cache.whitelistedContracts, c, whitelistedContract.Compare)
	if !ok {
		cache.whitelistedContracts = slices.Insert(cache.whitelistedContracts, i, c)
	}

	err = ic.AddNotification(p.Hash, "WhitelistFeeChanged", stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(h.BytesBE()),
		stackitem.NewByteArray([]byte(method)),
		stackitem.NewBigInteger(big.NewInt(int64(argCnt))),
		stackitem.NewBigInteger(big.NewInt(fee)),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.Null{}
}

func (p *Policy) getWhitelistFeeContracts(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(p.ID).(*PolicyCache)
	res := make([]stackitem.Item, len(cache.whitelistedContracts))
	for i, c := range cache.whitelistedContracts {
		res[i], _ = c.ToStackItem() // never returns an error.
	}
	return stackitem.NewInterop(&iterator[stackitem.Item]{keys: res})
}

func makeWhitelistedKey(h util.Uint160, offset uint32) []byte {
	k := make([]byte, 1+util.Uint160Size+4)
	k[0] = whitelistedFeeContractPrefix
	copy(k[1:], h.BytesBE())
	binary.BigEndian.PutUint32(k[1+util.Uint160Size:], uint32(offset))
	return k
}

// iterator provides an iterator over a slice of T. T must be convertible to
// [stackitem.Item] via [stackitem.Make].
type iterator[T any] struct {
	keys []T
	next bool
}

// Next advances the iterator and returns true if Value can be called at the
// current position.
func (i *iterator[T]) Next() bool {
	if i.next {
		i.keys = i.keys[1:]
	}
	i.next = len(i.keys) > 0
	return i.next
}

// Value returns current iterators value.
func (i *iterator[T]) Value() stackitem.Item {
	if !i.next {
		panic("iterator index out of range")
	}
	return stackitem.Make(i.keys[0])
}
