package interop

import (
	"cmp"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/interopnames"
	"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/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"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/smartcontract/nef"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"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/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"go.uber.org/zap"
)

const (
	// DefaultBaseExecFee specifies the default multiplier for opcode and syscall prices.
	DefaultBaseExecFee = 30
	// ContextNonceDataLen is a length of [Context.NonceData] in bytes.
	ContextNonceDataLen = 16
	// MaxNotificationCount is the maximum number of notifications per single
	// application execution.
	MaxNotificationCount = 512
)

// Ledger is the interface to Blockchain required for Context functionality.
type Ledger interface {
	BlockHeight() uint32
	CurrentBlockHash() util.Uint256
	GetBlock(hash util.Uint256) (*block.Block, error)
	GetConfig() config.Blockchain
	GetHeaderHash(uint32) util.Uint256
	NativeManagementID() int32
}

// PolicyChecker is an interface required for checking native Policy constraints.
type PolicyChecker interface {
	// IsBlocked returns whether the provided account is blocked by network policy.
	IsBlocked(d *dao.Simple, hash util.Uint160) bool
	// WhitelistedFee returns whether the specified contract method is whitelisted.
	// Non-negative return value defines the execution fee of a whitelisted method
	// call in picoGAS units.
	WhitelistedFee(d *dao.Simple, hash util.Uint160, offset int) int64
}

// Context represents context in which interops are executed.
type Context struct {
	Chain         Ledger
	Container     hash.Hashable
	Network       uint32
	Hardforks     map[string]uint32
	Natives       []Contract
	PolicyChecker PolicyChecker
	Trigger       trigger.Type
	Block         *block.Block
	// IsBlockPersisted denotes whether current Block was persisted by native
	// Ledger contract via PostPersist method.
	IsBlockPersisted bool
	NonceData        [ContextNonceDataLen]byte
	Tx               *transaction.Transaction
	DAO              *dao.Simple
	Notifications    []state.NotificationEvent
	Log              *zap.Logger
	VM               *vm.VM
	Functions        []Function
	Invocations      map[util.Uint160]int
	InvocationCalls  []state.ContractInvocation
	cancelFuncs      []context.CancelFunc
	getContract      func(*dao.Simple, util.Uint160) (*state.Contract, error)
	// baseExecFee is a factor to multiply the cost of execution in picoGAS units.
	baseExecFee int64
	// baseStorageFee is a factor to multiply the cost of storing 1 byte of data
	// in picoGAS units.
	baseStorageFee   int64
	loadToken        func(ic *Context, id int32) error
	GetRandomCounter uint32
	signers          []transaction.Signer
	SaveInvocations  bool
}

// NewContext returns new interop context. It expects baseExecFee and
// baseStorageFee to be represented in picoGAS units.
func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, baseStorageFee int64,
	getContract func(*dao.Simple, util.Uint160) (*state.Contract, error), natives []Contract,
	loadTokenFunc func(ic *Context, id int32) error,
	block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context {
	var (
		dao = d.GetPrivate()
		cfg = bc.GetConfig()
		pch PolicyChecker
	)
	for _, c := range natives {
		if c.Metadata().Name == nativenames.Policy {
			pch = c.(PolicyChecker)
		}
	}
	return &Context{
		Chain:           bc,
		Network:         uint32(cfg.Magic),
		Hardforks:       cfg.Hardforks,
		Natives:         natives,
		PolicyChecker:   pch,
		Trigger:         trigger,
		Block:           block,
		Tx:              tx,
		DAO:             dao,
		Log:             log,
		Invocations:     make(map[util.Uint160]int),
		getContract:     getContract,
		baseExecFee:     baseExecFee,
		baseStorageFee:  baseStorageFee,
		loadToken:       loadTokenFunc,
		SaveInvocations: cfg.SaveInvocations,
	}
}

// InitNonceData initializes nonce to be used in `GetRandom` calculations.
func (ic *Context) InitNonceData() {
	if tx, ok := ic.Container.(*transaction.Transaction); ok {
		ic.NonceData = [ContextNonceDataLen]byte(tx.Hash().BytesBE())
	}
	if ic.Block != nil {
		nonce := ic.Block.Nonce
		nonce ^= binary.LittleEndian.Uint64(ic.NonceData[:])
		binary.LittleEndian.PutUint64(ic.NonceData[:], nonce)
	}
}

// UseSigners allows overriding signers used in this context.
func (ic *Context) UseSigners(s []transaction.Signer) {
	ic.signers = s
}

// Signers returns signers witnessing the current execution context.
func (ic *Context) Signers() []transaction.Signer {
	if ic.signers != nil {
		return ic.signers
	}
	if ic.Tx != nil {
		return ic.Tx.Signers
	}
	return nil
}

// Function binds function name, id with the function itself and the price,
// it's supposed to be inited once for all interopContexts, so it doesn't use
// vm.InteropFuncPrice directly.
type Function struct {
	ID         uint32
	Name       string
	Func       func(*Context) error
	Price      int64
	ActiveFrom config.Hardfork
	// RequiredFlags is a set of flags which must be set during script invocations.
	// Default value is NoneFlag i.e. no flags are required.
	RequiredFlags callflag.CallFlag
}

// Method is a signature for a native method.
type Method = func(ic *Context, args []stackitem.Item) stackitem.Item

// MethodAndPrice is a generic hardfork-independent native contract method descriptor.
type MethodAndPrice struct {
	HFSpecificMethodAndPrice
	ActiveFrom *config.Hardfork
	ActiveTill *config.Hardfork
}

// HFSpecificMethodAndPrice is a hardfork-specific native contract method descriptor.
type HFSpecificMethodAndPrice struct {
	Func          Method
	MD            *manifest.Method
	CPUFee        int64
	StorageFee    int64
	SyscallOffset int
	RequiredFlags callflag.CallFlag
}

// Event is a generic hardfork-independent native contract event descriptor.
type Event struct {
	HFSpecificEvent
	ActiveFrom *config.Hardfork
	ActiveTill *config.Hardfork
}

// HFSpecificEvent is a hardfork-specific native contract event descriptor.
type HFSpecificEvent struct {
	MD *manifest.Event
}

// Contract is an interface for all native contracts.
type Contract interface {
	// Initialize performs native contract initialization on contract deploy or update.
	// Active hardfork is passed as the second argument.
	Initialize(*Context, *config.Hardfork, *HFSpecificContractMD) error
	// ActiveIn returns the hardfork native contract is active starting from or nil in case
	// it's always active.
	ActiveIn() *config.Hardfork
	// InitializeCache aimed to initialize contract's cache when the contract has
	// been deployed, but in-memory cached data were lost due to the node reset.
	// It should be called each time after node restart iff the contract was
	// deployed and no Initialize method was called.
	InitializeCache(isHardforkEnabled IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error
	// Metadata returns generic native contract metadata.
	Metadata() *ContractMD
	OnPersist(*Context) error
	PostPersist(*Context) error
}

// IsHardforkEnabled is a delegate denoting whether specific hardfork is enabled
// at the given height.
type IsHardforkEnabled func(hf *config.Hardfork, blockHeight uint32) bool

// ContractMD represents a generic hardfork-independent native contract instance.
type ContractMD struct {
	ID   int32
	Hash util.Uint160
	Name string
	// methods is a generic set of contract methods with activation hardforks. Any HF-dependent part of included methods
	// (offsets, in particular) must not be used, there's an MDCache field for that.
	methods []MethodAndPrice
	// Events is a generic set of contract events with activation hardforks. Any HF-dependent part of events must not be
	// used, there's an MDCache field for that.
	events []Event
	// ActiveHFs is a map of hardforks that contract should react to. Contract update should be called for active
	// hardforks. Note, that unlike the C# implementation, this map doesn't include contract's activation hardfork.
	// This map is being initialized on contract creation and used as a read-only, hence, not protected
	// by mutex.
	ActiveHFs map[config.Hardfork]struct{}

	// mdCache contains hardfork-specific ready-to-use contract descriptors. This cache is initialized in the native
	// contracts constructors, and acts as read-only during the whole node lifetime, thus not protected by mutex.
	mdCache map[config.Hardfork]*HFSpecificContractMD

	// onManifestConstruction is a callback for manifest finalization.
	onManifestConstruction func(*manifest.Manifest, config.Hardfork)
}

// HFSpecificContractMD is a hardfork-specific native contract descriptor.
type HFSpecificContractMD struct {
	state.ContractBase
	Methods []HFSpecificMethodAndPrice
	Events  []HFSpecificEvent
}

// NewContractMD returns Contract with the specified fields set. onManifestConstruction callback every time
// after hardfork-specific manifest creation and aimed to finalize the manifest.
func NewContractMD(name string, id int32, onManifestConstruction ...func(*manifest.Manifest, config.Hardfork)) *ContractMD {
	c := &ContractMD{Name: name}
	if len(onManifestConstruction) != 0 {
		c.onManifestConstruction = onManifestConstruction[0]
	}

	c.ID = id
	c.Hash = state.CreateNativeContractHash(c.Name)
	c.ActiveHFs = make(map[config.Hardfork]struct{})
	c.mdCache = make(map[config.Hardfork]*HFSpecificContractMD)

	return c
}

// HFSpecificContractMD returns hardfork-specific native contract metadata, i.e. with methods, events and script
// corresponding to the specified hardfork. If hardfork is not specified, then default metadata will be returned
// (methods, events and script that are always active). Calling this method for hardforks older than the contract
// activation hardfork is a no-op.
func (c *ContractMD) HFSpecificContractMD(hf *config.Hardfork) *HFSpecificContractMD {
	var key config.Hardfork
	if hf != nil {
		key = *hf
	}
	md, ok := c.mdCache[key]
	if !ok {
		panic(fmt.Errorf("native contract descriptor cache is not initialized: contract %s, hardfork %s", c.Name, key))
	}
	if md == nil {
		panic(fmt.Errorf("native contract descriptor cache is nil: contract %s, hardfork %s", c.Name, key))
	}
	return md
}

// BuildHFSpecificMD generates and caches contract's descriptor for every known hardfork.
func (c *ContractMD) BuildHFSpecificMD(activeIn *config.Hardfork) {
	var start config.Hardfork
	if activeIn != nil {
		start = *activeIn
	}

	for _, hf := range append([]config.Hardfork{config.HFDefault}, config.Hardforks...) {
		if hf.Cmp(start) < 0 {
			continue
		}
		_, contractHasChanges := c.ActiveHFs[hf]
		if hf.Cmp(start) == 0 || contractHasChanges {
			c.buildHFSpecificMD(hf)
		} else {
			// Optimize out MD rebuild, the contract is the same.
			// Intentionally omit HFSpecificContractMD structure copying since mdCache is read-only.
			c.mdCache[hf] = c.mdCache[hf.Prev()]
		}
	}
}

// buildHFSpecificMD builds hardfork-specific contract descriptor that includes methods and events active starting from
// the specified hardfork or older. It also updates cache with the received value.
func (c *ContractMD) buildHFSpecificMD(hf config.Hardfork) {
	var (
		abiMethods = make([]manifest.Method, 0, len(c.methods))
		methods    = make([]HFSpecificMethodAndPrice, 0, len(c.methods))
		abiEvents  = make([]manifest.Event, 0, len(c.events))
		events     = make([]HFSpecificEvent, 0, len(c.events))
	)
	w := io.NewBufBinWriter()
	for i := range c.methods {
		m := c.methods[i]
		if (m.ActiveFrom != nil && (*m.ActiveFrom).Cmp(hf) > 0) ||
			(m.ActiveTill != nil && (*m.ActiveTill).Cmp(hf) <= 0) {
			continue
		}

		// Perform method descriptor copy to support independent HF-based offset update.
		md := *m.MD
		m.MD = &md
		m.MD.Offset = w.Len()

		emit.Int(w.BinWriter, 0)
		m.SyscallOffset = w.Len()
		emit.Syscall(w.BinWriter, interopnames.SystemContractCallNative)
		emit.Opcodes(w.BinWriter, opcode.RET)

		abiMethods = append(abiMethods, *m.MD)
		methods = append(methods, m.HFSpecificMethodAndPrice)
	}
	if w.Err != nil {
		panic(fmt.Errorf("can't create native contract script: %w", w.Err))
	}
	for i := range c.events {
		e := c.events[i]
		if (e.ActiveFrom != nil && (*e.ActiveFrom).Cmp(hf) > 0) ||
			(e.ActiveTill != nil && (*e.ActiveTill).Cmp(hf) <= 0) {
			continue
		}

		abiEvents = append(abiEvents, *e.MD)
		events = append(events, e.HFSpecificEvent)
	}

	// NEF is now stored in the contract state and affects state dump.
	// Therefore, values are taken from C# node.
	nf := nef.File{
		Header: nef.Header{
			Magic:    nef.Magic,
			Compiler: "neo-core-v3.0",
		},
		Tokens: []nef.MethodToken{}, // avoid `nil` result during JSON marshalling,
		Script: w.Bytes(),
	}
	nf.Checksum = nf.CalculateChecksum()
	m := manifest.DefaultManifest(c.Name)
	m.ABI.Methods = abiMethods
	m.ABI.Events = abiEvents
	if c.onManifestConstruction != nil {
		c.onManifestConstruction(m, hf)
	}
	md := &HFSpecificContractMD{
		ContractBase: state.ContractBase{
			ID:       c.ID,
			Hash:     c.Hash,
			NEF:      nf,
			Manifest: *m,
		},
		Methods: methods,
		Events:  events,
	}

	c.mdCache[hf] = md
}

// AddMethod adds a new method to a native contract.
func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method) {
	md.MD = desc
	desc.Safe = md.RequiredFlags&(callflag.All^callflag.ReadOnly) == 0

	index, _ := slices.BinarySearchFunc(c.methods, *md, func(e, t MethodAndPrice) int {
		return cmp.Or(
			cmp.Compare(e.MD.Name, t.MD.Name),
			cmp.Compare(len(e.MD.Parameters), len(t.MD.Parameters)),
		)
	})
	c.methods = slices.Insert(c.methods, index, *md)

	if md.ActiveFrom != nil {
		c.ActiveHFs[*md.ActiveFrom] = struct{}{}
	}
	if md.ActiveTill != nil {
		c.ActiveHFs[*md.ActiveTill] = struct{}{}
	}
}

// GetMethodByOffset returns method with the provided offset.
// Offset is offset of `System.Contract.CallNative` syscall.
func (c *HFSpecificContractMD) GetMethodByOffset(offset int) (HFSpecificMethodAndPrice, bool) {
	for k := range c.Methods {
		if c.Methods[k].SyscallOffset == offset {
			return c.Methods[k], true
		}
	}
	return HFSpecificMethodAndPrice{}, false
}

// GetMethod returns method `name` with the specified number of parameters.
func (c *HFSpecificContractMD) GetMethod(name string, paramCount int) (HFSpecificMethodAndPrice, bool) {
	index, ok := slices.BinarySearchFunc(c.Methods, HFSpecificMethodAndPrice{}, func(a, _ HFSpecificMethodAndPrice) int {
		res := cmp.Compare(a.MD.Name, name)
		if res != 0 {
			return res
		}
		return cmp.Compare(len(a.MD.Parameters), paramCount)
	})
	// Exact match is possible only for specific paramCount, but if we're
	// searching for _some_ method with this name (-1) we're taking the
	// first one.
	if ok || (index < len(c.Methods) && c.Methods[index].MD.Name == name && paramCount == -1) {
		return c.Methods[index], true
	}
	return HFSpecificMethodAndPrice{}, false
}

// AddEvent adds a new event to the native contract.
func (c *ContractMD) AddEvent(md Event) {
	c.events = append(c.events, md)

	if md.ActiveFrom != nil {
		c.ActiveHFs[*md.ActiveFrom] = struct{}{}
	}
	if md.ActiveTill != nil {
		c.ActiveHFs[*md.ActiveTill] = struct{}{}
	}
}

// Sort sorts interop functions by id.
func Sort(fs []Function) {
	slices.SortFunc(fs, func(a, b Function) int { return cmp.Compare(a.ID, b.ID) })
}

// GetContract returns a contract by its hash in the current interop context.
func (ic *Context) GetContract(hash util.Uint160) (*state.Contract, error) {
	return ic.getContract(ic.DAO, hash)
}

// GetFunction returns metadata for interop with the specified id.
func (ic *Context) GetFunction(id uint32) *Function {
	n, ok := slices.BinarySearchFunc(ic.Functions, Function{}, func(a, _ Function) int {
		return cmp.Compare(a.ID, id)
	})
	if !ok || (ic.Functions[n].ActiveFrom != config.HFDefault && !ic.IsHardforkEnabled(ic.Functions[n].ActiveFrom)) {
		return nil
	}
	return &ic.Functions[n]
}

// BaseExecFee represents a factor to multiply syscall prices with (in picoGAS
// units).
func (ic *Context) BaseExecFee() int64 {
	return ic.baseExecFee
}

// BaseStorageFee represents a price for storing one byte of data in the
// contract storage in picoGAS units.
func (ic *Context) BaseStorageFee() int64 {
	return ic.baseStorageFee
}

// LoadToken wraps externally provided load-token loading function providing it with context,
// this function can then be easily used by VM.
func (ic *Context) LoadToken(id int32) error {
	return ic.loadToken(ic, id)
}

// SyscallHandler handles syscall with id.
func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error {
	f := ic.GetFunction(id)
	if f == nil {
		return errors.New("syscall not found")
	}
	cf := ic.VM.Context().GetCallFlags()
	if !cf.Has(f.RequiredFlags) {
		return fmt.Errorf("missing call flags: %05b vs %05b", cf, f.RequiredFlags)
	}
	price := f.Price * ic.BaseExecFee()
	if !ic.VM.AddPicoGas(price) {
		return errors.New("insufficient amount of gas")
	}
	return f.Func(ic)
}

// SpawnVM spawns a new VM with the specified gas limit and set context.VM field.
func (ic *Context) SpawnVM() *vm.VM {
	v := vm.NewWithTrigger(ic.Trigger)
	ic.initVM(v)
	return v
}

func (ic *Context) initVM(v *vm.VM) {
	v.LoadToken = ic.LoadToken
	v.SetGasLimit(-1)
	v.SyscallHandler = ic.SyscallHandler
	v.SetPriceGetter(ic.GetPrice)
	ic.VM = v
}

// ReuseVM resets given VM and allows to reuse it in the current context.
func (ic *Context) ReuseVM(v *vm.VM) {
	v.Reset(ic.Trigger)
	ic.initVM(v)
}

// RegisterCancelFunc adds the given function to the list of functions to be called after the VM
// finishes script execution.
func (ic *Context) RegisterCancelFunc(f context.CancelFunc) {
	if f != nil {
		ic.cancelFuncs = append(ic.cancelFuncs, f)
	}
}

// Finalize calls all registered cancel functions to release the occupied resources.
func (ic *Context) Finalize() {
	for _, f := range ic.cancelFuncs {
		f()
	}
	ic.cancelFuncs = nil
}

// Exec executes loaded VM script and calls registered finalizers to release the occupied resources.
func (ic *Context) Exec() error {
	defer ic.Finalize()
	return ic.VM.Run()
}

// BlockHeight returns the latest persisted and stored block height/index.
// The latest persisted block is updated by native Ledger's PostPersist, hence
// persisting block index is not taken into account until the call to Ledger's
// PostPersist. If Context's block is set, then BlockHeight calculations rely
// on index of this block.
func (ic *Context) BlockHeight() uint32 {
	if ic.Block != nil {
		if ic.IsBlockPersisted {
			return ic.Block.Index // Persisting block is already processed by native Ledger.
		}
		return ic.Block.Index - 1 // Persisting block is not yet processed.
	}
	if ic.IsBlockPersisted {
		return ic.Chain.BlockHeight() + 1
	}
	return ic.Chain.BlockHeight()
}

// persistingBlockHeight returns the height/index of persisting block irrespectively
// of native Ledger's PostPersist call.
func (ic *Context) persistingBlockHeight() uint32 {
	if ic.Block != nil {
		return ic.Block.Index
	}
	return ic.Chain.BlockHeight() + 1
}

// CurrentBlockHash returns current block hash got from Context's block if it's set.
func (ic *Context) CurrentBlockHash() util.Uint256 {
	if ic.Block != nil {
		return ic.Chain.GetHeaderHash(ic.BlockHeight())
	}
	return ic.Chain.CurrentBlockHash()
}

// GetBlock returns block if it exists and available at the current Context's height.
func (ic *Context) GetBlock(hash util.Uint256) (*block.Block, error) {
	block, err := ic.Chain.GetBlock(hash)
	if err != nil {
		return nil, err
	}
	if block.Index > ic.BlockHeight() { // persisting block is not reachable.
		return nil, storage.ErrKeyNotFound
	}
	return block, nil
}

// IsHardforkEnabled tells whether specified hard-fork enabled at the current context height.
func (ic *Context) IsHardforkEnabled(hf config.Hardfork) bool {
	height, ok := ic.Hardforks[hf.String()]
	if ok {
		return ic.persistingBlockHeight() >= height // persisting block should be taken into account.
	}
	// Completely rely on proper hardforks initialisation made by core.NewBlockchain.
	return false
}

// IsHardforkActivation denotes whether current block height is the height of
// specified hardfork activation.
func (ic *Context) IsHardforkActivation(hf config.Hardfork) bool {
	// Completely rely on proper hardforks initialisation made by core.NewBlockchain.
	height, ok := ic.Hardforks[hf.String()]
	return ok && ic.Block.Index == height
}

// AddNotification creates notification event and appends it to the notification list.
func (ic *Context) AddNotification(hash util.Uint160, name string, item *stackitem.Array) error {
	if ic.IsHardforkEnabled(config.HFEchidna) {
		// Do not check persisting triggers to avoid native persist failure. Do not check
		// verification trigger since verification context is loaded with ReadOnly flag.
		if ic.Trigger == trigger.Application && len(ic.Notifications) == MaxNotificationCount {
			return fmt.Errorf("notification count shouldn't exceed %d", MaxNotificationCount)
		}
	}
	ic.Notifications = append(ic.Notifications, state.NotificationEvent{
		ScriptHash: hash,
		Name:       name,
		Item:       item,
	})
	return nil
}
