package vm

import (
	"bytes"
	"context"
	"encoding/binary"
	"fmt"
	"os"
	"time"

	"github.com/ipfs/go-cid"
	ipldcbor "github.com/ipfs/go-ipld-cbor"
	"go.opencensus.io/trace"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/abi"
	actorstypes "github.com/filecoin-project/go-state-types/actors"
	"github.com/filecoin-project/go-state-types/cbor"
	"github.com/filecoin-project/go-state-types/crypto"
	"github.com/filecoin-project/go-state-types/exitcode"
	"github.com/filecoin-project/go-state-types/network"
	rtt "github.com/filecoin-project/go-state-types/rt"
	rt0 "github.com/filecoin-project/specs-actors/actors/runtime"
	rt2 "github.com/filecoin-project/specs-actors/v2/actors/runtime"
	rt3 "github.com/filecoin-project/specs-actors/v3/actors/runtime"
	rt4 "github.com/filecoin-project/specs-actors/v4/actors/runtime"
	rt5 "github.com/filecoin-project/specs-actors/v5/actors/runtime"
	rt6 "github.com/filecoin-project/specs-actors/v6/actors/runtime"
	rt7 "github.com/filecoin-project/specs-actors/v7/actors/runtime"

	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/actors"
	"github.com/filecoin-project/lotus/chain/actors/aerrors"
	"github.com/filecoin-project/lotus/chain/actors/builtin"
	"github.com/filecoin-project/lotus/chain/rand"
	"github.com/filecoin-project/lotus/chain/state"
	"github.com/filecoin-project/lotus/chain/types"
)

type Message struct {
	msg types.Message
}

func (m *Message) Caller() address.Address {
	if m.msg.From.Protocol() != address.ID {
		panic("runtime message has a non-ID caller")
	}
	return m.msg.From
}

func (m *Message) Receiver() address.Address {
	if m.msg.To != address.Undef && m.msg.To.Protocol() != address.ID {
		panic("runtime message has a non-ID receiver")
	}
	return m.msg.To
}

func (m *Message) ValueReceived() abi.TokenAmount {
	return m.msg.Value
}

// EnableDetailedTracing has different behaviour in the LegacyVM and FVM.
// In the LegacyVM, it enables detailed gas tracing, slowing down execution.
// In the FVM, it enables execution traces, which are primarily used to observe subcalls.
var EnableDetailedTracing = os.Getenv("LOTUS_VM_ENABLE_TRACING") == "1"

type Runtime struct {
	rt7.Message
	rt7.Syscalls

	ctx context.Context

	vm        *LegacyVM
	state     *state.StateTree
	height    abi.ChainEpoch
	cst       ipldcbor.IpldStore
	pricelist Pricelist

	gasAvailable int64
	gasUsed      int64

	// address that started invoke chain
	origin      address.Address
	originNonce uint64

	executionTrace    types.ExecutionTrace
	depth             uint64
	numActorsCreated  uint64
	allowInternal     bool
	callerValidated   bool
	lastGasChargeTime time.Time
	lastGasCharge     *types.GasTrace
}

func (rt *Runtime) BaseFee() abi.TokenAmount {
	return rt.vm.baseFee
}

func (rt *Runtime) NetworkVersion() network.Version {
	return rt.vm.networkVersion
}

func (rt *Runtime) TotalFilCircSupply() abi.TokenAmount {
	cs, err := rt.vm.GetCircSupply(rt.ctx)
	if err != nil {
		rt.Abortf(exitcode.ErrIllegalState, "failed to get total circ supply: %s", err)
	}

	return cs
}

func (rt *Runtime) ResolveAddress(addr address.Address) (ret address.Address, ok bool) {
	r, err := rt.state.LookupID(addr)
	if err != nil {
		if xerrors.Is(err, types.ErrActorNotFound) {
			return address.Undef, false
		}
		panic(aerrors.Fatalf("failed to resolve address %s: %s", addr, err))
	}
	return r, true
}

type notFoundErr interface {
	IsNotFound() bool
}

func (rt *Runtime) StoreGet(c cid.Cid, o cbor.Unmarshaler) bool {
	if err := rt.cst.Get(context.TODO(), c, o); err != nil {
		var nfe notFoundErr
		if xerrors.As(err, &nfe) && nfe.IsNotFound() {
			if xerrors.As(err, new(ipldcbor.SerializationError)) {
				panic(aerrors.Newf(exitcode.ErrSerialization, "failed to unmarshal cbor object %s", err))
			}
			return false
		}

		panic(aerrors.Fatalf("failed to get cbor object %s: %s", c, err))
	}
	return true
}

func (rt *Runtime) StorePut(x cbor.Marshaler) cid.Cid {
	c, err := rt.cst.Put(context.TODO(), x)
	if err != nil {
		if xerrors.As(err, new(ipldcbor.SerializationError)) {
			panic(aerrors.Newf(exitcode.ErrSerialization, "failed to marshal cbor object %s", err))
		}
		panic(aerrors.Fatalf("failed to put cbor object: %s", err))
	}
	return c
}

var _ rt0.Runtime = (*Runtime)(nil)
var _ rt5.Runtime = (*Runtime)(nil)
var _ rt2.Runtime = (*Runtime)(nil)
var _ rt3.Runtime = (*Runtime)(nil)
var _ rt4.Runtime = (*Runtime)(nil)
var _ rt5.Runtime = (*Runtime)(nil)
var _ rt6.Runtime = (*Runtime)(nil)
var _ rt7.Runtime = (*Runtime)(nil)

func (rt *Runtime) shimCall(f func() interface{}) (rval []byte, aerr aerrors.ActorError) {
	defer func() {
		if r := recover(); r != nil {
			if ar, ok := r.(aerrors.ActorError); ok {
				log.Warnf("LegacyVM.Call failure in call from: %s to %s: %+v", rt.Caller(), rt.Receiver(), ar)
				aerr = ar
				return
			}
			// log.Desugar().WithOptions(zap.AddStacktrace(zapcore.ErrorLevel)).
			// Sugar().Errorf("spec actors failure: %s", r)
			log.Errorf("spec actors failure: %s", r)
			if rt.NetworkVersion() <= network.Version3 {
				aerr = aerrors.Newf(1, "spec actors failure: %s", r)
			} else {
				aerr = aerrors.Newf(exitcode.SysErrIllegalInstruction, "spec actors failure: %s", r)
			}
		}
	}()

	ret := f()

	if !rt.callerValidated {
		rt.Abortf(exitcode.SysErrorIllegalActor, "Caller MUST be validated during method execution")
	}

	switch ret := ret.(type) {
	case []byte:
		return ret, nil
	case *abi.EmptyValue:
		return nil, nil
	case cbor.Marshaler:
		buf := new(bytes.Buffer)
		if err := ret.MarshalCBOR(buf); err != nil {
			return nil, aerrors.Absorb(err, 2, "failed to marshal response to cbor")
		}
		return buf.Bytes(), nil
	case nil:
		return nil, nil
	default:
		return nil, aerrors.New(3, "could not determine type for response from call")
	}
}

func (rt *Runtime) ValidateImmediateCallerAcceptAny() {
	rt.abortIfAlreadyValidated()
	return
}

func (rt *Runtime) CurrentBalance() abi.TokenAmount {
	b, err := rt.GetBalance(rt.Receiver())
	if err != nil {
		rt.Abortf(err.RetCode(), "get current balance: %v", err)
	}
	return b
}

func (rt *Runtime) GetActorCodeCID(addr address.Address) (ret cid.Cid, ok bool) {
	act, err := rt.state.GetActor(addr)
	if err != nil {
		if xerrors.Is(err, types.ErrActorNotFound) {
			return cid.Undef, false
		}

		panic(aerrors.Fatalf("failed to get actor: %s", err))
	}

	return act.Code, true
}

func (rt *Runtime) GetRandomnessFromTickets(personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte) abi.Randomness {
	digest, err := rt.vm.rand.GetChainRandomness(rt.ctx, randEpoch)

	if err != nil {
		panic(aerrors.Fatalf("could not get ticket randomness: %s", err))
	}

	ret, err := rand.DrawRandomnessFromDigest(digest, personalization, randEpoch, entropy)

	if err != nil {
		panic(aerrors.Fatalf("could not draw ticket randomness: %s", err))
	}

	return ret
}

func (rt *Runtime) GetRandomnessFromBeacon(personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte) abi.Randomness {
	digest, err := rt.vm.rand.GetBeaconRandomness(rt.ctx, randEpoch)

	if err != nil {
		panic(aerrors.Fatalf("could not get ticket randomness: %s", err))
	}

	ret, err := rand.DrawRandomnessFromDigest(digest, personalization, randEpoch, entropy)

	if err != nil {
		panic(aerrors.Fatalf("could not draw ticket randomness: %s", err))
	}

	return ret
}

func (rt *Runtime) NewActorAddress() address.Address {
	var b bytes.Buffer
	oa, _ := ResolveToDeterministicAddr(rt.vm.cstate, rt.vm.cst, rt.origin)
	if err := oa.MarshalCBOR(&b); err != nil { // todo: spec says cbor; why not just bytes?
		panic(aerrors.Fatalf("writing caller address into a buffer: %v", err))
	}

	if err := binary.Write(&b, binary.BigEndian, rt.originNonce); err != nil {
		panic(aerrors.Fatalf("writing nonce address into a buffer: %v", err))
	}
	if err := binary.Write(&b, binary.BigEndian, rt.numActorsCreated); err != nil { // TODO: expose on vm
		panic(aerrors.Fatalf("writing callSeqNum address into a buffer: %v", err))
	}
	addr, err := address.NewActorAddress(b.Bytes())
	if err != nil {
		panic(aerrors.Fatalf("create actor address: %v", err))
	}

	rt.incrementNumActorsCreated()
	return addr
}

func (rt *Runtime) CreateActor(codeID cid.Cid, addr address.Address) {
	if addr == address.Undef && rt.NetworkVersion() >= network.Version7 {
		rt.Abortf(exitcode.SysErrorIllegalArgument, "CreateActor with Undef address")
	}
	act, aerr := rt.vm.areg.Create(codeID, rt)
	if aerr != nil {
		rt.Abortf(aerr.RetCode(), aerr.Error())
	}

	_, err := rt.state.GetActor(addr)
	if err == nil {
		rt.Abortf(exitcode.SysErrorIllegalArgument, "Actor address already exists")
	}

	rt.chargeGas(rt.Pricelist().OnCreateActor())

	err = rt.state.SetActor(addr, act)
	if err != nil {
		panic(aerrors.Fatalf("creating actor entry: %v", err))
	}
	_ = rt.chargeGasSafe(gasOnActorExec)
}

// DeleteActor deletes the executing actor from the state tree, transferring
// any balance to beneficiary.
// Aborts if the beneficiary does not exist or is the calling actor.
// May only be called by the actor itself.
func (rt *Runtime) DeleteActor(beneficiary address.Address) {
	rt.chargeGas(rt.Pricelist().OnDeleteActor())
	act, err := rt.state.GetActor(rt.Receiver())
	if err != nil {
		if xerrors.Is(err, types.ErrActorNotFound) {
			rt.Abortf(exitcode.SysErrorIllegalActor, "failed to load actor in delete actor: %s", err)
		}
		panic(aerrors.Fatalf("failed to get actor: %s", err))
	}
	if !act.Balance.IsZero() {
		// TODO: Should be safe to drop the version-check,
		//  since only the paych actor called this pre-version 7, but let's leave it for now
		if rt.NetworkVersion() >= network.Version7 {
			beneficiaryId, found := rt.ResolveAddress(beneficiary)
			if !found {
				rt.Abortf(exitcode.SysErrorIllegalArgument, "beneficiary doesn't exist")
			}

			if beneficiaryId == rt.Receiver() {
				rt.Abortf(exitcode.SysErrorIllegalArgument, "benefactor cannot be beneficiary")
			}
		}

		// Transfer the executing actor's balance to the beneficiary
		if err := rt.vm.transfer(rt.Receiver(), beneficiary, act.Balance, rt.NetworkVersion()); err != nil {
			panic(aerrors.Fatalf("failed to transfer balance to beneficiary actor: %s", err))
		}
	}

	// Delete the executing actor
	if err := rt.state.DeleteActor(rt.Receiver()); err != nil {
		panic(aerrors.Fatalf("failed to delete actor: %s", err))
	}
	_ = rt.chargeGasSafe(gasOnActorExec)
}

func (rt *Runtime) StartSpan(name string) func() {
	panic("implement me")
}

func (rt *Runtime) ValidateImmediateCallerIs(as ...address.Address) {
	rt.abortIfAlreadyValidated()
	imm := rt.Caller()

	for _, a := range as {
		if imm == a {
			return
		}
	}
	rt.Abortf(exitcode.SysErrForbidden, "caller %s is not one of %s", rt.Caller(), as)
}

func (rt *Runtime) Context() context.Context {
	return rt.ctx
}

func (rt *Runtime) Abortf(code exitcode.ExitCode, msg string, args ...interface{}) {
	log.Warnf("Abortf: " + fmt.Sprintf(msg, args...))
	panic(aerrors.NewfSkip(2, code, msg, args...))
}

func (rt *Runtime) AbortStateMsg(msg string) {
	panic(aerrors.NewfSkip(3, 101, msg))
}

func (rt *Runtime) ValidateImmediateCallerType(ts ...cid.Cid) {
	rt.abortIfAlreadyValidated()
	callerCid, ok := rt.GetActorCodeCID(rt.Caller())
	if !ok {
		panic(aerrors.Fatalf("failed to lookup code cid for caller"))
	}
	for _, t := range ts {
		if t == callerCid {
			return
		}

		// this really only for genesis in tests; nv16 will be running on FVM anyway.
		if nv := rt.NetworkVersion(); nv >= network.Version16 {
			av, err := actorstypes.VersionForNetwork(nv)
			if err != nil {
				panic(aerrors.Fatalf("failed to get actors version for network version %d", nv))
			}

			name := actors.CanonicalName(builtin.ActorNameByCode(t))
			ac, ok := actors.GetActorCodeID(av, name)
			if ok && ac == callerCid {
				return
			}
		}
	}
	rt.Abortf(exitcode.SysErrForbidden, "caller cid type %q was not one of %v", callerCid, ts)
}

func (rt *Runtime) CurrEpoch() abi.ChainEpoch {
	return rt.height
}

func (rt *Runtime) Send(to address.Address, method abi.MethodNum, m cbor.Marshaler, value abi.TokenAmount, out cbor.Er) exitcode.ExitCode {
	if !rt.allowInternal {
		rt.Abortf(exitcode.SysErrorIllegalActor, "runtime.Send() is currently disallowed")
	}
	var params []byte
	if m != nil {
		buf := new(bytes.Buffer)
		if err := m.MarshalCBOR(buf); err != nil {
			rt.Abortf(exitcode.ErrSerialization, "failed to marshal input parameters: %s", err)
		}
		params = buf.Bytes()
	}

	ret, err := rt.internalSend(rt.Receiver(), to, method, value, params)
	if err != nil {
		if err.IsFatal() {
			panic(err)
		}
		log.Warnf("vmctx send failed: from: %s to: %s, method: %d: err: %s", rt.Receiver(), to, method, err)
		return err.RetCode()
	}
	_ = rt.chargeGasSafe(gasOnActorExec)

	if err := out.UnmarshalCBOR(bytes.NewReader(ret)); err != nil {
		rt.Abortf(exitcode.ErrSerialization, "failed to unmarshal return value: %s", err)
	}
	return 0
}

func (rt *Runtime) internalSend(from, to address.Address, method abi.MethodNum, value types.BigInt, params []byte) ([]byte, aerrors.ActorError) {
	start := build.Clock.Now()
	ctx, span := trace.StartSpan(rt.ctx, "vmc.Send")
	defer span.End()
	if span.IsRecordingEvents() {
		span.AddAttributes(
			trace.StringAttribute("to", to.String()),
			trace.Int64Attribute("method", int64(method)),
			trace.StringAttribute("value", value.String()),
		)
	}

	msg := &types.Message{
		From:     from,
		To:       to,
		Method:   method,
		Value:    value,
		Params:   params,
		GasLimit: rt.gasAvailable,
	}

	st := rt.state
	if err := st.Snapshot(ctx); err != nil {
		return nil, aerrors.Fatalf("snapshot failed: %s", err)
	}
	defer st.ClearSnapshot()

	ret, errSend, subrt := rt.vm.send(ctx, msg, rt, nil, start)
	if errSend != nil {
		if errRevert := st.Revert(); errRevert != nil {
			return nil, aerrors.Escalate(errRevert, "failed to revert state tree after failed subcall")
		}
	}

	if subrt != nil {
		rt.numActorsCreated = subrt.numActorsCreated
		rt.executionTrace.Subcalls = append(rt.executionTrace.Subcalls, subrt.executionTrace)
	}
	return ret, errSend
}

func (rt *Runtime) StateCreate(obj cbor.Marshaler) {
	c := rt.StorePut(obj)
	err := rt.stateCommit(EmptyObjectCid, c)
	if err != nil {
		panic(fmt.Errorf("failed to commit state after creating object: %w", err))
	}
}

func (rt *Runtime) StateReadonly(obj cbor.Unmarshaler) {
	act, err := rt.state.GetActor(rt.Receiver())
	if err != nil {
		rt.Abortf(exitcode.SysErrorIllegalArgument, "failed to get actor for Readonly state: %s", err)
	}
	rt.StoreGet(act.Head, obj)
}

func (rt *Runtime) StateTransaction(obj cbor.Er, f func()) {
	if obj == nil {
		rt.Abortf(exitcode.SysErrorIllegalActor, "Must not pass nil to Transaction()")
	}

	act, err := rt.state.GetActor(rt.Receiver())
	if err != nil {
		rt.Abortf(exitcode.SysErrorIllegalActor, "failed to get actor for Transaction: %s", err)
	}
	baseState := act.Head
	rt.StoreGet(baseState, obj)

	rt.allowInternal = false
	f()
	rt.allowInternal = true

	c := rt.StorePut(obj)

	err = rt.stateCommit(baseState, c)
	if err != nil {
		panic(fmt.Errorf("failed to commit state after transaction: %w", err))
	}
}

func (rt *Runtime) GetBalance(a address.Address) (types.BigInt, aerrors.ActorError) {
	act, err := rt.state.GetActor(a)
	switch err {
	default:
		return types.EmptyInt, aerrors.Escalate(err, "failed to look up actor balance")
	case types.ErrActorNotFound:
		return types.NewInt(0), nil
	case nil:
		return act.Balance, nil
	}
}

func (rt *Runtime) stateCommit(oldh, newh cid.Cid) aerrors.ActorError {
	// TODO: we can make this more efficient in the future...
	act, err := rt.state.GetActor(rt.Receiver())
	if err != nil {
		return aerrors.Escalate(err, "failed to get actor to commit state")
	}

	if act.Head != oldh {
		return aerrors.Fatal("failed to update, inconsistent base reference")
	}

	act.Head = newh

	if err := rt.state.SetActor(rt.Receiver(), act); err != nil {
		return aerrors.Fatalf("failed to set actor in commit state: %s", err)
	}

	return nil
}

func (rt *Runtime) finilizeGasTracing() {
	if EnableDetailedTracing {
		if rt.lastGasCharge != nil {
			rt.lastGasCharge.TimeTaken = time.Since(rt.lastGasChargeTime)
		}
	}
}

// ChargeGas is spec actors function
func (rt *Runtime) ChargeGas(name string, compute int64, virtual int64) {
	err := rt.chargeGasInternal(newGasCharge(name, compute, 0).WithVirtual(virtual, 0), 1)
	if err != nil {
		panic(err)
	}
}

func (rt *Runtime) chargeGas(gas GasCharge) {
	err := rt.chargeGasInternal(gas, 1)
	if err != nil {
		panic(err)
	}
}

func (rt *Runtime) chargeGasFunc(skip int) func(GasCharge) {
	return func(gas GasCharge) {
		err := rt.chargeGasInternal(gas, 1+skip)
		if err != nil {
			panic(err)
		}
	}

}

func (rt *Runtime) chargeGasInternal(gas GasCharge, skip int) aerrors.ActorError {
	toUse := gas.Total()
	if EnableDetailedTracing {
		now := build.Clock.Now()
		if rt.lastGasCharge != nil {
			rt.lastGasCharge.TimeTaken = now.Sub(rt.lastGasChargeTime)
		}

		gasTrace := types.GasTrace{
			Name: gas.Name,

			TotalGas:   toUse,
			ComputeGas: gas.ComputeGas,
			StorageGas: gas.StorageGas,
		}

		rt.executionTrace.GasCharges = append(rt.executionTrace.GasCharges, &gasTrace)
		rt.lastGasChargeTime = now
		rt.lastGasCharge = &gasTrace
	}

	// overflow safe
	if rt.gasUsed > rt.gasAvailable-toUse {
		gasUsed := rt.gasUsed
		rt.gasUsed = rt.gasAvailable
		return aerrors.Newf(exitcode.SysErrOutOfGas, "not enough gas: used=%d, available=%d, use=%d",
			gasUsed, rt.gasAvailable, toUse)
	}
	rt.gasUsed += toUse
	return nil
}

func (rt *Runtime) chargeGasSafe(gas GasCharge) aerrors.ActorError {
	return rt.chargeGasInternal(gas, 1)
}

func (rt *Runtime) Pricelist() Pricelist {
	return rt.pricelist
}

func (rt *Runtime) incrementNumActorsCreated() {
	rt.numActorsCreated++
}

func (rt *Runtime) abortIfAlreadyValidated() {
	if rt.callerValidated {
		rt.Abortf(exitcode.SysErrorIllegalActor, "Method must validate caller identity exactly once")
	}
	rt.callerValidated = true
}

func (rt *Runtime) Log(level rtt.LogLevel, msg string, args ...interface{}) {
	switch level {
	case rtt.DEBUG:
		actorLog.Debugf(msg, args...)
	case rtt.INFO:
		actorLog.Infof(msg, args...)
	case rtt.WARN:
		actorLog.Warnf(msg, args...)
	case rtt.ERROR:
		actorLog.Errorf(msg, args...)
	}
}