package vm

import (
	"bytes"
	"context"
	"fmt"
	"sync/atomic"
	"time"

	"github.com/ipfs/go-cid"
	cbor "github.com/ipfs/go-ipld-cbor"
	block "github.com/ipfs/go-libipfs/blocks"
	logging "github.com/ipfs/go-log/v2"
	mh "github.com/multiformats/go-multihash"
	cbg "github.com/whyrusleeping/cbor-gen"
	"go.opencensus.io/stats"
	"go.opencensus.io/trace"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"
	builtin_types "github.com/filecoin-project/go-state-types/builtin"
	"github.com/filecoin-project/go-state-types/crypto"
	"github.com/filecoin-project/go-state-types/exitcode"
	"github.com/filecoin-project/go-state-types/network"

	"github.com/filecoin-project/lotus/blockstore"
	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/actors/adt"
	"github.com/filecoin-project/lotus/chain/actors/aerrors"
	"github.com/filecoin-project/lotus/chain/actors/builtin"
	"github.com/filecoin-project/lotus/chain/actors/builtin/account"
	"github.com/filecoin-project/lotus/chain/actors/builtin/reward"
	"github.com/filecoin-project/lotus/chain/state"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/metrics"
)

const MaxCallDepth = 4096
const CborCodec = 0x51

var (
	log            = logging.Logger("vm")
	actorLog       = logging.WithSkip(logging.Logger("actors"), 1)
	gasOnActorExec = newGasCharge("OnActorExec", 0, 0)
)

// ResolveToDeterministicAddr returns the public key type of address
// (`BLS`/`SECP256K1`) of an actor identified by `addr`, or its
// delegated address.
func ResolveToDeterministicAddr(state types.StateTree, cst cbor.IpldStore, addr address.Address) (address.Address, error) {
	if addr.Protocol() == address.BLS || addr.Protocol() == address.SECP256K1 || addr.Protocol() == address.Delegated {
		return addr, nil
	}

	act, err := state.GetActor(addr)
	if err != nil {
		return address.Undef, xerrors.Errorf("failed to find actor: %s", addr)
	}

	if state.Version() >= types.StateTreeVersion5 {
		if act.Address != nil {
			// If there _is_ an f4 address, return it as "key" address
			return *act.Address, nil
		}
	}

	aast, err := account.Load(adt.WrapStore(context.TODO(), cst), act)
	if err != nil {
		return address.Undef, xerrors.Errorf("failed to get account actor state for %s: %w", addr, err)
	}
	return aast.PubkeyAddress()

}

var (
	_ cbor.IpldBlockstore = (*gasChargingBlocks)(nil)
	_ blockstore.Viewer   = (*gasChargingBlocks)(nil)
)

type gasChargingBlocks struct {
	chargeGas func(GasCharge)
	pricelist Pricelist
	under     cbor.IpldBlockstore
}

func (bs *gasChargingBlocks) View(ctx context.Context, c cid.Cid, cb func([]byte) error) error {
	if v, ok := bs.under.(blockstore.Viewer); ok {
		bs.chargeGas(bs.pricelist.OnIpldGet())
		return v.View(ctx, c, func(b []byte) error {
			// we have successfully retrieved the value; charge for it, even if the user-provided function fails.
			bs.chargeGas(newGasCharge("OnIpldViewEnd", 0, 0).WithExtra(len(b)))
			bs.chargeGas(gasOnActorExec)
			return cb(b)
		})
	}
	// the underlying blockstore doesn't implement the viewer interface, fall back to normal Get behaviour.
	blk, err := bs.Get(ctx, c)
	if err == nil && blk != nil {
		return cb(blk.RawData())
	}
	return err
}

func (bs *gasChargingBlocks) Get(ctx context.Context, c cid.Cid) (block.Block, error) {
	bs.chargeGas(bs.pricelist.OnIpldGet())
	blk, err := bs.under.Get(ctx, c)
	if err != nil {
		return nil, aerrors.Escalate(err, "failed to get block from blockstore")
	}
	bs.chargeGas(newGasCharge("OnIpldGetEnd", 0, 0).WithExtra(len(blk.RawData())))
	bs.chargeGas(gasOnActorExec)

	return blk, nil
}

func (bs *gasChargingBlocks) Put(ctx context.Context, blk block.Block) error {
	bs.chargeGas(bs.pricelist.OnIpldPut(len(blk.RawData())))

	if err := bs.under.Put(ctx, blk); err != nil {
		return aerrors.Escalate(err, "failed to write data to disk")
	}
	bs.chargeGas(gasOnActorExec)
	return nil
}

func (vm *LegacyVM) makeRuntime(ctx context.Context, msg *types.Message, parent *Runtime) *Runtime {
	paramsCodec := uint64(0)
	if len(msg.Params) > 0 {
		paramsCodec = CborCodec
	}
	rt := &Runtime{
		ctx:         ctx,
		vm:          vm,
		state:       vm.cstate,
		origin:      msg.From,
		originNonce: msg.Nonce,
		height:      vm.blockHeight,

		gasUsed:          0,
		gasAvailable:     msg.GasLimit,
		depth:            0,
		numActorsCreated: 0,
		pricelist:        PricelistByEpoch(vm.blockHeight),
		allowInternal:    true,
		callerValidated:  false,
		executionTrace: types.ExecutionTrace{Msg: types.MessageTrace{
			From:        msg.From,
			To:          msg.To,
			Value:       msg.Value,
			Method:      msg.Method,
			Params:      msg.Params,
			ParamsCodec: paramsCodec,
		}},
	}

	if parent != nil {
		// TODO: The version check here should be unnecessary, but we can wait to take it out
		if !parent.allowInternal && rt.NetworkVersion() >= network.Version7 {
			rt.Abortf(exitcode.SysErrForbidden, "internal calls currently disabled")
		}
		rt.gasUsed = parent.gasUsed
		rt.origin = parent.origin
		rt.originNonce = parent.originNonce
		rt.numActorsCreated = parent.numActorsCreated
		rt.depth = parent.depth + 1
	}

	if rt.depth > MaxCallDepth && rt.NetworkVersion() >= network.Version6 {
		rt.Abortf(exitcode.SysErrForbidden, "message execution exceeds call depth")
	}

	cbb := &gasChargingBlocks{rt.chargeGasFunc(2), rt.pricelist, vm.cst.Blocks}
	cst := cbor.NewCborStore(cbb)
	cst.Atlas = vm.cst.Atlas // associate the atlas.
	rt.cst = cst

	vmm := *msg
	resF, ok := rt.ResolveAddress(msg.From)
	if !ok {
		rt.Abortf(exitcode.SysErrInvalidReceiver, "resolve msg.From address failed")
	}
	vmm.From = resF

	if vm.networkVersion <= network.Version3 {
		rt.Message = &vmm
	} else {
		resT, _ := rt.ResolveAddress(msg.To)
		// may be set to undef if recipient doesn't exist yet
		vmm.To = resT
		rt.Message = &Message{msg: vmm}
	}

	rt.Syscalls = pricedSyscalls{
		under:     vm.Syscalls(ctx, rt),
		chargeGas: rt.chargeGasFunc(1),
		pl:        rt.pricelist,
	}

	return rt
}

type UnsafeVM struct {
	VM *LegacyVM
}

func (vm *UnsafeVM) MakeRuntime(ctx context.Context, msg *types.Message) *Runtime {
	return vm.VM.makeRuntime(ctx, msg, nil)
}

type (
	CircSupplyCalculator func(context.Context, abi.ChainEpoch, *state.StateTree) (abi.TokenAmount, error)
	NtwkVersionGetter    func(context.Context, abi.ChainEpoch) network.Version
	LookbackStateGetter  func(context.Context, abi.ChainEpoch) (*state.StateTree, error)
	TipSetGetter         func(context.Context, abi.ChainEpoch) (types.TipSetKey, error)
)

var _ Interface = (*LegacyVM)(nil)

type LegacyVM struct {
	cstate         *state.StateTree
	cst            *cbor.BasicIpldStore
	buf            *blockstore.BufferedBlockstore
	blockHeight    abi.ChainEpoch
	areg           *ActorRegistry
	rand           Rand
	circSupplyCalc CircSupplyCalculator
	networkVersion network.Version
	baseFee        abi.TokenAmount
	lbStateGet     LookbackStateGetter
	baseCircSupply abi.TokenAmount

	Syscalls SyscallBuilder
}

type VMOpts struct {
	StateBase      cid.Cid
	Epoch          abi.ChainEpoch
	Timestamp      uint64
	Rand           Rand
	Bstore         blockstore.Blockstore
	Actors         *ActorRegistry
	Syscalls       SyscallBuilder
	CircSupplyCalc CircSupplyCalculator
	NetworkVersion network.Version
	BaseFee        abi.TokenAmount
	LookbackState  LookbackStateGetter
	TipSetGetter   TipSetGetter
	Tracing        bool
	// ReturnEvents decodes and returns emitted events.
	ReturnEvents bool
	// ExecutionLane specifies the execution priority of the created vm
	ExecutionLane ExecutionLane
}

func NewLegacyVM(ctx context.Context, opts *VMOpts) (*LegacyVM, error) {
	if opts.NetworkVersion >= network.Version16 {
		return nil, xerrors.Errorf("the legacy VM does not support network versions 16+")
	}

	buf := blockstore.NewBuffered(opts.Bstore)
	cst := cbor.NewCborStore(buf)
	state, err := state.LoadStateTree(cst, opts.StateBase)
	if err != nil {
		return nil, err
	}

	baseCirc, err := opts.CircSupplyCalc(ctx, opts.Epoch, state)
	if err != nil {
		return nil, err
	}

	return &LegacyVM{
		cstate:         state,
		cst:            cst,
		buf:            buf,
		blockHeight:    opts.Epoch,
		areg:           opts.Actors,
		rand:           opts.Rand, // TODO: Probably should be a syscall
		circSupplyCalc: opts.CircSupplyCalc,
		networkVersion: opts.NetworkVersion,
		Syscalls:       opts.Syscalls,
		baseFee:        opts.BaseFee,
		baseCircSupply: baseCirc,
		lbStateGet:     opts.LookbackState,
	}, nil
}

type Rand interface {
	GetChainRandomness(ctx context.Context, pers crypto.DomainSeparationTag, round abi.ChainEpoch, entropy []byte) ([]byte, error)
	GetBeaconRandomness(ctx context.Context, pers crypto.DomainSeparationTag, round abi.ChainEpoch, entropy []byte) ([]byte, error)
}

type ApplyRet struct {
	types.MessageReceipt
	ActorErr       aerrors.ActorError
	ExecutionTrace types.ExecutionTrace
	Duration       time.Duration
	GasCosts       *GasOutputs
	Events         []types.Event
}

func (vm *LegacyVM) send(ctx context.Context, msg *types.Message, parent *Runtime,
	gasCharge *GasCharge, start time.Time) ([]byte, aerrors.ActorError, *Runtime) {
	defer atomic.AddUint64(&StatSends, 1)

	st := vm.cstate

	rt := vm.makeRuntime(ctx, msg, parent)
	if EnableDetailedTracing {
		rt.lastGasChargeTime = start
		if parent != nil {
			rt.lastGasChargeTime = parent.lastGasChargeTime
			rt.lastGasCharge = parent.lastGasCharge
			defer func() {
				parent.lastGasChargeTime = rt.lastGasChargeTime
				parent.lastGasCharge = rt.lastGasCharge
			}()
		}
	}

	if parent != nil {
		defer func() {
			parent.gasUsed = rt.gasUsed
		}()
	}
	if gasCharge != nil {
		if err := rt.chargeGasSafe(*gasCharge); err != nil {
			// this should never happen
			return nil, aerrors.Wrap(err, "not enough gas for initial message charge, this should not happen"), rt
		}
	}

	ret, err := func() ([]byte, aerrors.ActorError) {
		_ = rt.chargeGasSafe(newGasCharge("OnGetActor", 0, 0))
		toActor, err := st.GetActor(msg.To)
		if err != nil {
			if xerrors.Is(err, types.ErrActorNotFound) {
				a, aid, err := TryCreateAccountActor(rt, msg.To)
				if err != nil {
					return nil, aerrors.Wrapf(err, "could not create account")
				}
				toActor = a
				if vm.networkVersion <= network.Version3 {
					// Leave the rt.Message as is
				} else {
					nmsg := Message{
						msg: types.Message{
							To:    aid,
							From:  rt.Message.Caller(),
							Value: rt.Message.ValueReceived(),
						},
					}

					rt.Message = &nmsg
				}
			} else {
				return nil, aerrors.Escalate(err, "getting actor")
			}
		}

		if aerr := rt.chargeGasSafe(rt.Pricelist().OnMethodInvocation(msg.Value, msg.Method)); aerr != nil {
			return nil, aerrors.Wrap(aerr, "not enough gas for method invocation")
		}

		// not charging any gas, just logging
		//nolint:errcheck
		defer rt.chargeGasSafe(newGasCharge("OnMethodInvocationDone", 0, 0))

		if types.BigCmp(msg.Value, types.NewInt(0)) != 0 {
			if err := vm.transfer(msg.From, msg.To, msg.Value, vm.networkVersion); err != nil {
				return nil, aerrors.Wrap(err, "failed to transfer funds")
			}
		}

		if msg.Method != 0 {
			var ret []byte
			_ = rt.chargeGasSafe(gasOnActorExec)
			ret, err := vm.Invoke(toActor, rt, msg.Method, msg.Params)
			return ret, err
		}
		return nil, nil
	}()

	retCodec := uint64(0)
	if len(ret) > 0 {
		retCodec = CborCodec
	}
	rt.executionTrace.MsgRct = types.ReturnTrace{
		ExitCode:    aerrors.RetCode(err),
		Return:      ret,
		ReturnCodec: retCodec,
	}

	return ret, err, rt
}

func checkMessage(msg *types.Message) error {
	if msg.GasLimit == 0 {
		return xerrors.Errorf("message has no gas limit set")
	}
	if msg.GasLimit < 0 {
		return xerrors.Errorf("message has negative gas limit")
	}

	if msg.GasFeeCap == types.EmptyInt {
		return xerrors.Errorf("message fee cap not set")
	}

	if msg.GasPremium == types.EmptyInt {
		return xerrors.Errorf("message gas premium not set")
	}

	if msg.Value == types.EmptyInt {
		return xerrors.Errorf("message no value set")
	}

	return nil
}

func (vm *LegacyVM) ApplyImplicitMessage(ctx context.Context, msg *types.Message) (*ApplyRet, error) {
	start := build.Clock.Now()
	defer atomic.AddUint64(&StatApplied, 1)
	ret, actorErr, rt := vm.send(ctx, msg, nil, nil, start)
	rt.finilizeGasTracing()
	return &ApplyRet{
		MessageReceipt: types.MessageReceipt{
			ExitCode: aerrors.RetCode(actorErr),
			Return:   ret,
			GasUsed:  0,
		},
		ActorErr:       actorErr,
		ExecutionTrace: rt.executionTrace,
		GasCosts:       nil,
		Duration:       time.Since(start),
	}, actorErr
}

func (vm *LegacyVM) ApplyMessage(ctx context.Context, cmsg types.ChainMsg) (*ApplyRet, error) {
	start := build.Clock.Now()
	ctx, span := trace.StartSpan(ctx, "vm.ApplyMessage")
	defer span.End()
	defer atomic.AddUint64(&StatApplied, 1)
	msg := cmsg.VMMessage()
	if span.IsRecordingEvents() {
		span.AddAttributes(
			trace.StringAttribute("to", msg.To.String()),
			trace.Int64Attribute("method", int64(msg.Method)),
			trace.StringAttribute("value", msg.Value.String()),
		)
	}

	if err := checkMessage(msg); err != nil {
		return nil, err
	}

	pl := PricelistByEpoch(vm.blockHeight)

	msgGas := pl.OnChainMessage(cmsg.ChainLength())
	msgGasCost := msgGas.Total()
	// this should never happen, but is currently still exercised by some tests
	if msgGasCost > msg.GasLimit {
		gasOutputs := ZeroGasOutputs()
		gasOutputs.MinerPenalty = types.BigMul(vm.baseFee, abi.NewTokenAmount(msgGasCost))
		return &ApplyRet{
			MessageReceipt: types.MessageReceipt{
				ExitCode: exitcode.SysErrOutOfGas,
				GasUsed:  0,
			},
			GasCosts: &gasOutputs,
			Duration: time.Since(start),
			ActorErr: aerrors.Newf(exitcode.SysErrOutOfGas,
				"message gas limit does not cover on-chain gas costs"),
		}, nil
	}

	st := vm.cstate

	minerPenaltyAmount := types.BigMul(vm.baseFee, abi.NewTokenAmount(msg.GasLimit))
	fromActor, err := st.GetActor(msg.From)
	// this should never happen, but is currently still exercised by some tests
	if err != nil {
		if xerrors.Is(err, types.ErrActorNotFound) {
			gasOutputs := ZeroGasOutputs()
			gasOutputs.MinerPenalty = minerPenaltyAmount
			return &ApplyRet{
				MessageReceipt: types.MessageReceipt{
					ExitCode: exitcode.SysErrSenderInvalid,
					GasUsed:  0,
				},
				ActorErr: aerrors.Newf(exitcode.SysErrSenderInvalid, "actor not found: %s", msg.From),
				GasCosts: &gasOutputs,
				Duration: time.Since(start),
			}, nil
		}
		return nil, xerrors.Errorf("failed to look up from actor: %w", err)
	}

	// this should never happen, but is currently still exercised by some tests
	if !builtin.IsAccountActor(fromActor.Code) {
		gasOutputs := ZeroGasOutputs()
		gasOutputs.MinerPenalty = minerPenaltyAmount
		return &ApplyRet{
			MessageReceipt: types.MessageReceipt{
				ExitCode: exitcode.SysErrSenderInvalid,
				GasUsed:  0,
			},
			ActorErr: aerrors.Newf(exitcode.SysErrSenderInvalid, "send from not account actor: %s", fromActor.Code),
			GasCosts: &gasOutputs,
			Duration: time.Since(start),
		}, nil
	}

	if msg.Nonce != fromActor.Nonce {
		gasOutputs := ZeroGasOutputs()
		gasOutputs.MinerPenalty = minerPenaltyAmount
		return &ApplyRet{
			MessageReceipt: types.MessageReceipt{
				ExitCode: exitcode.SysErrSenderStateInvalid,
				GasUsed:  0,
			},
			ActorErr: aerrors.Newf(exitcode.SysErrSenderStateInvalid,
				"actor nonce invalid: msg:%d != state:%d", msg.Nonce, fromActor.Nonce),

			GasCosts: &gasOutputs,
			Duration: time.Since(start),
		}, nil
	}

	gascost := types.BigMul(types.NewInt(uint64(msg.GasLimit)), msg.GasFeeCap)
	if fromActor.Balance.LessThan(gascost) {
		gasOutputs := ZeroGasOutputs()
		gasOutputs.MinerPenalty = minerPenaltyAmount
		return &ApplyRet{
			MessageReceipt: types.MessageReceipt{
				ExitCode: exitcode.SysErrSenderStateInvalid,
				GasUsed:  0,
			},
			ActorErr: aerrors.Newf(exitcode.SysErrSenderStateInvalid,
				"actor balance less than needed: %s < %s", types.FIL(fromActor.Balance), types.FIL(gascost)),
			GasCosts: &gasOutputs,
			Duration: time.Since(start),
		}, nil
	}

	gasHolder := &types.Actor{Balance: types.NewInt(0)}
	if err := vm.transferToGasHolder(msg.From, gasHolder, gascost); err != nil {
		return nil, xerrors.Errorf("failed to withdraw gas funds: %w", err)
	}

	if err := vm.incrementNonce(msg.From); err != nil {
		return nil, err
	}

	if err := st.Snapshot(ctx); err != nil {
		return nil, xerrors.Errorf("snapshot failed: %w", err)
	}
	defer st.ClearSnapshot()

	ret, actorErr, rt := vm.send(ctx, msg, nil, &msgGas, start)
	if aerrors.IsFatal(actorErr) {
		return nil, xerrors.Errorf("[from=%s,to=%s,n=%d,m=%d,h=%d] fatal error: %w", msg.From, msg.To, msg.Nonce, msg.Method, vm.blockHeight, actorErr)
	}

	if actorErr != nil {
		log.Warnw("Send actor error", "from", msg.From, "to", msg.To, "nonce", msg.Nonce, "method", msg.Method, "height", vm.blockHeight, "error", fmt.Sprintf("%+v", actorErr))
	}

	if actorErr != nil && len(ret) != 0 {
		// This should not happen, something is wonky
		return nil, xerrors.Errorf("message invocation errored, but had a return value anyway: %w", actorErr)
	}

	if rt == nil {
		return nil, xerrors.Errorf("send returned nil runtime, send error was: %s", actorErr)
	}

	if len(ret) != 0 {
		// safely override actorErr since it must be nil
		actorErr = rt.chargeGasSafe(rt.Pricelist().OnChainReturnValue(len(ret)))
		if actorErr != nil {
			ret = nil
		}
	}

	var errcode exitcode.ExitCode
	var gasUsed int64

	if errcode = aerrors.RetCode(actorErr); errcode != 0 {
		// revert all state changes since snapshot
		if err := st.Revert(); err != nil {
			return nil, xerrors.Errorf("revert state failed: %w", err)
		}
	}

	rt.finilizeGasTracing()

	gasUsed = rt.gasUsed
	if gasUsed < 0 {
		gasUsed = 0
	}

	burn, err := vm.ShouldBurn(ctx, st, msg, errcode)
	if err != nil {
		return nil, xerrors.Errorf("deciding whether should burn failed: %w", err)
	}

	gasOutputs := ComputeGasOutputs(gasUsed, msg.GasLimit, vm.baseFee, msg.GasFeeCap, msg.GasPremium, burn)

	if err := vm.transferFromGasHolder(builtin.BurntFundsActorAddr, gasHolder,
		gasOutputs.BaseFeeBurn); err != nil {
		return nil, xerrors.Errorf("failed to burn base fee: %w", err)
	}

	if err := vm.transferFromGasHolder(reward.Address, gasHolder, gasOutputs.MinerTip); err != nil {
		return nil, xerrors.Errorf("failed to give miner gas reward: %w", err)
	}

	if err := vm.transferFromGasHolder(builtin.BurntFundsActorAddr, gasHolder,
		gasOutputs.OverEstimationBurn); err != nil {
		return nil, xerrors.Errorf("failed to burn overestimation fee: %w", err)
	}

	// refund unused gas
	if err := vm.transferFromGasHolder(msg.From, gasHolder, gasOutputs.Refund); err != nil {
		return nil, xerrors.Errorf("failed to refund gas: %w", err)
	}

	if types.BigCmp(types.NewInt(0), gasHolder.Balance) != 0 {
		return nil, xerrors.Errorf("gas handling math is wrong")
	}

	return &ApplyRet{
		MessageReceipt: types.MessageReceipt{
			ExitCode: errcode,
			Return:   ret,
			GasUsed:  gasUsed,
		},
		ActorErr:       actorErr,
		ExecutionTrace: rt.executionTrace,
		GasCosts:       &gasOutputs,
		Duration:       time.Since(start),
	}, nil
}

func (vm *LegacyVM) ShouldBurn(ctx context.Context, st *state.StateTree, msg *types.Message, errcode exitcode.ExitCode) (bool, error) {
	if vm.networkVersion <= network.Version12 {
		// Check to see if we should burn funds. We avoid burning on successful
		// window post. This won't catch _indirect_ window post calls, but this
		// is the best we can get for now.
		if vm.blockHeight > build.UpgradeClausHeight && errcode == exitcode.Ok && msg.Method == builtin_types.MethodsMiner.SubmitWindowedPoSt {
			// Ok, we've checked the _method_, but we still need to check
			// the target actor. It would be nice if we could just look at
			// the trace, but I'm not sure if that's safe?
			if toActor, err := st.GetActor(msg.To); err != nil {
				// If the actor wasn't found, we probably deleted it or something. Move on.
				if !xerrors.Is(err, types.ErrActorNotFound) {
					// Otherwise, this should never fail and something is very wrong.
					return false, xerrors.Errorf("failed to lookup target actor: %w", err)
				}
			} else if builtin.IsStorageMinerActor(toActor.Code) {
				// Ok, this is a storage miner and we've processed a window post. Remove the burn.
				return false, nil
			}
		}

		return true, nil
	}

	// Any "don't burn" rules from Network v13 onwards go here, for now we always return true
	return true, nil
}

type vmFlushKey struct{}

func (vm *LegacyVM) Flush(ctx context.Context) (cid.Cid, error) {
	_, span := trace.StartSpan(ctx, "vm.Flush")
	defer span.End()

	from := vm.buf
	to := vm.buf.Read()

	root, err := vm.cstate.Flush(ctx)
	if err != nil {
		return cid.Undef, xerrors.Errorf("flushing vm: %w", err)
	}

	if err := Copy(context.WithValue(ctx, vmFlushKey{}, true), from, to, root); err != nil {
		return cid.Undef, xerrors.Errorf("copying tree: %w", err)
	}

	return root, nil
}

// Get the buffered blockstore associated with the LegacyVM. This includes any temporary blocks produced
// during this LegacyVM's execution.
func (vm *LegacyVM) ActorStore(ctx context.Context) adt.Store {
	return adt.WrapStore(ctx, vm.cst)
}

func linksForObj(blk block.Block, cb func(cid.Cid)) error {
	switch blk.Cid().Prefix().Codec {
	case cid.DagCBOR:
		err := cbg.ScanForLinks(bytes.NewReader(blk.RawData()), cb)
		if err != nil {
			return xerrors.Errorf("cbg.ScanForLinks: %w", err)
		}
		return nil
	case cid.Raw:
		// We implicitly have all children of raw blocks.
		return nil
	default:
		return xerrors.Errorf("vm flush copy method only supports dag cbor")
	}
}

func Copy(ctx context.Context, from, to blockstore.Blockstore, root cid.Cid) error {
	ctx, span := trace.StartSpan(ctx, "vm.Copy") // nolint
	defer span.End()
	start := time.Now()

	var numBlocks int
	var totalCopySize int

	const batchSize = 128
	const bufCount = 3
	freeBufs := make(chan []block.Block, bufCount)
	toFlush := make(chan []block.Block, bufCount)
	for i := 0; i < bufCount; i++ {
		freeBufs <- make([]block.Block, 0, batchSize)
	}

	errFlushChan := make(chan error)

	go func() {
		for b := range toFlush {
			if err := to.PutMany(ctx, b); err != nil {
				close(freeBufs)
				errFlushChan <- xerrors.Errorf("batch put in copy: %w", err)
				return
			}
			freeBufs <- b[:0]
		}
		close(errFlushChan)
		close(freeBufs)
	}()

	batch := <-freeBufs
	batchCp := func(blk block.Block) error {
		numBlocks++
		totalCopySize += len(blk.RawData())

		batch = append(batch, blk)

		if len(batch) >= batchSize {
			toFlush <- batch
			var ok bool
			batch, ok = <-freeBufs
			if !ok {
				return <-errFlushChan
			}
		}
		return nil
	}

	if err := copyRec(ctx, from, to, root, batchCp); err != nil {
		return xerrors.Errorf("copyRec: %w", err)
	}

	if len(batch) > 0 {
		toFlush <- batch
	}
	close(toFlush)        // close the toFlush triggering the loop to end
	err := <-errFlushChan // get error out or get nil if it was closed
	if err != nil {
		return err
	}

	span.AddAttributes(
		trace.Int64Attribute("numBlocks", int64(numBlocks)),
		trace.Int64Attribute("copySize", int64(totalCopySize)),
	)
	if yes, ok := ctx.Value(vmFlushKey{}).(bool); yes && ok {
		took := metrics.SinceInMilliseconds(start)
		stats.Record(ctx, metrics.VMFlushCopyCount.M(int64(numBlocks)), metrics.VMFlushCopyDuration.M(took))
	}

	return nil
}

func copyRec(ctx context.Context, from, to blockstore.Blockstore, root cid.Cid, cp func(block.Block) error) error {
	if root.Prefix().MhType == 0 {
		// identity cid, skip
		return nil
	}

	blk, err := from.Get(ctx, root)
	if err != nil {
		return xerrors.Errorf("get %s failed: %w", root, err)
	}

	var lerr error
	err = linksForObj(blk, func(link cid.Cid) {
		if lerr != nil {
			// Theres no erorr return on linksForObj callback :(
			return
		}

		prefix := link.Prefix()
		if prefix.Codec == cid.FilCommitmentSealed || prefix.Codec == cid.FilCommitmentUnsealed {
			return
		}

		// We always have blocks inlined into CIDs, but we may not have their children.
		if prefix.MhType == mh.IDENTITY {
			// Unless the inlined block has no children.
			if prefix.Codec == cid.Raw {
				return
			}
		} else {
			// If we have an object, we already have its children, skip the object.
			has, err := to.Has(ctx, link)
			if err != nil {
				lerr = xerrors.Errorf("has: %w", err)
				return
			}
			if has {
				return
			}
		}

		if err := copyRec(ctx, from, to, link, cp); err != nil {
			lerr = err
			return
		}
	})
	if err != nil {
		return xerrors.Errorf("linksForObj (%x): %w", blk.RawData(), err)
	}
	if lerr != nil {
		return lerr
	}

	if err := cp(blk); err != nil {
		return xerrors.Errorf("copy: %w", err)
	}
	return nil
}

func (vm *LegacyVM) StateTree() types.StateTree {
	return vm.cstate
}

func (vm *LegacyVM) Invoke(act *types.Actor, rt *Runtime, method abi.MethodNum, params []byte) ([]byte, aerrors.ActorError) {
	ctx, span := trace.StartSpan(rt.ctx, "vm.Invoke")
	defer span.End()
	if span.IsRecordingEvents() {
		span.AddAttributes(
			trace.StringAttribute("to", rt.Receiver().String()),
			trace.Int64Attribute("method", int64(method)),
			trace.StringAttribute("value", rt.ValueReceived().String()),
		)
	}

	var oldCtx context.Context
	oldCtx, rt.ctx = rt.ctx, ctx
	defer func() {
		rt.ctx = oldCtx
	}()
	ret, err := vm.areg.Invoke(act.Code, rt, method, params)
	if err != nil {
		return nil, err
	}
	return ret, nil
}

func (vm *LegacyVM) SetInvoker(i *ActorRegistry) {
	vm.areg = i
}

func (vm *LegacyVM) GetCircSupply(ctx context.Context) (abi.TokenAmount, error) {
	// Before v15, this was recalculated on each invocation as the state tree was mutated
	if vm.networkVersion <= network.Version14 {
		return vm.circSupplyCalc(ctx, vm.blockHeight, vm.cstate)
	}

	return vm.baseCircSupply, nil
}

func (vm *LegacyVM) incrementNonce(addr address.Address) error {
	return vm.cstate.MutateActor(addr, func(a *types.Actor) error {
		a.Nonce++
		return nil
	})
}

func (vm *LegacyVM) transfer(from, to address.Address, amt types.BigInt, networkVersion network.Version) aerrors.ActorError {
	var f *types.Actor
	var fromID, toID address.Address
	var err error
	// switching the order around so that transactions for more than the balance sent to self fail
	if networkVersion >= network.Version15 {
		if amt.LessThan(types.NewInt(0)) {
			return aerrors.Newf(exitcode.SysErrForbidden, "attempted to transfer negative value: %s", amt)
		}

		fromID, err = vm.cstate.LookupID(from)
		if err != nil {
			return aerrors.Fatalf("transfer failed when resolving sender address: %s", err)
		}

		f, err = vm.cstate.GetActor(fromID)
		if err != nil {
			return aerrors.Fatalf("transfer failed when retrieving sender actor: %s", err)
		}

		if f.Balance.LessThan(amt) {
			return aerrors.Newf(exitcode.SysErrInsufficientFunds, "transfer failed, insufficient balance in sender actor: %v", f.Balance)
		}

		if from == to {
			log.Infow("sending to same address: noop", "from/to addr", from)
			return nil
		}

		toID, err = vm.cstate.LookupID(to)
		if err != nil {
			return aerrors.Fatalf("transfer failed when resolving receiver address: %s", err)
		}

		if fromID == toID {
			log.Infow("sending to same actor ID: noop", "from/to actor", fromID)
			return nil
		}
	} else {
		if from == to {
			return nil
		}

		fromID, err = vm.cstate.LookupID(from)
		if err != nil {
			return aerrors.Fatalf("transfer failed when resolving sender address: %s", err)
		}

		toID, err = vm.cstate.LookupID(to)
		if err != nil {
			return aerrors.Fatalf("transfer failed when resolving receiver address: %s", err)
		}

		if fromID == toID {
			return nil
		}

		if amt.LessThan(types.NewInt(0)) {
			return aerrors.Newf(exitcode.SysErrForbidden, "attempted to transfer negative value: %s", amt)
		}

		f, err = vm.cstate.GetActor(fromID)
		if err != nil {
			return aerrors.Fatalf("transfer failed when retrieving sender actor: %s", err)
		}
	}

	t, err := vm.cstate.GetActor(toID)
	if err != nil {
		return aerrors.Fatalf("transfer failed when retrieving receiver actor: %s", err)
	}

	if err = deductFunds(f, amt); err != nil {
		return aerrors.Newf(exitcode.SysErrInsufficientFunds, "transfer failed when deducting funds (%s): %s", types.FIL(amt), err)
	}
	depositFunds(t, amt)

	if err = vm.cstate.SetActor(fromID, f); err != nil {
		return aerrors.Fatalf("transfer failed when setting sender actor: %s", err)
	}

	if err = vm.cstate.SetActor(toID, t); err != nil {
		return aerrors.Fatalf("transfer failed when setting receiver actor: %s", err)
	}

	return nil
}

func (vm *LegacyVM) transferToGasHolder(addr address.Address, gasHolder *types.Actor, amt types.BigInt) error {
	if amt.LessThan(types.NewInt(0)) {
		return xerrors.Errorf("attempted to transfer negative value to gas holder")
	}

	return vm.cstate.MutateActor(addr, func(a *types.Actor) error {
		if err := deductFunds(a, amt); err != nil {
			return err
		}
		depositFunds(gasHolder, amt)
		return nil
	})
}

func (vm *LegacyVM) transferFromGasHolder(addr address.Address, gasHolder *types.Actor, amt types.BigInt) error {
	if amt.LessThan(types.NewInt(0)) {
		return xerrors.Errorf("attempted to transfer negative value from gas holder")
	}

	if amt.Equals(big.NewInt(0)) {
		return nil
	}

	return vm.cstate.MutateActor(addr, func(a *types.Actor) error {
		if err := deductFunds(gasHolder, amt); err != nil {
			return err
		}
		depositFunds(a, amt)
		return nil
	})
}

func deductFunds(act *types.Actor, amt types.BigInt) error {
	if act.Balance.LessThan(amt) {
		return fmt.Errorf("not enough funds")
	}

	act.Balance = types.BigSub(act.Balance, amt)
	return nil
}

func depositFunds(act *types.Actor, amt types.BigInt) {
	act.Balance = types.BigAdd(act.Balance, amt)
}
