package genesis

import (
	"context"
	"fmt"

	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	actorstypes "github.com/filecoin-project/go-state-types/actors"
	"github.com/filecoin-project/go-state-types/big"
	"github.com/filecoin-project/go-state-types/builtin"
	"github.com/filecoin-project/go-state-types/manifest"
	"github.com/filecoin-project/go-state-types/network"

	"github.com/filecoin-project/lotus/chain/actors"
	"github.com/filecoin-project/lotus/chain/actors/adt"
	init_ "github.com/filecoin-project/lotus/chain/actors/builtin/init"
	"github.com/filecoin-project/lotus/chain/state"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/chain/types/ethtypes"
	"github.com/filecoin-project/lotus/chain/vm"
)

// EthNullAddresses are the Ethereum addresses we want to create zero-balanced EthAccounts in.
// We may want to add null addresses for precompiles going forward.
var EthNullAddresses = []string{
	"0x0000000000000000000000000000000000000000",
}

func SetupEAM(_ context.Context, nst *state.StateTree, nv network.Version) error {
	av, err := actorstypes.VersionForNetwork(nv)
	if err != nil {
		return fmt.Errorf("failed to get actors version for network version %d: %w", nv, err)
	}

	if av < actorstypes.Version10 {
		// Not defined before version 10; migration has to create.
		return nil
	}

	codecid, ok := actors.GetActorCodeID(av, manifest.EamKey)
	if !ok {
		return fmt.Errorf("failed to get CodeCID for EAM during genesis")
	}

	header := &types.Actor{
		Code:    codecid,
		Head:    vm.EmptyObjectCid,
		Balance: big.Zero(),
		Address: &builtin.EthereumAddressManagerActorAddr, // so that it can create ETH0
	}
	return nst.SetActor(builtin.EthereumAddressManagerActorAddr, header)
}

// MakeEthNullAddressActor creates a null address actor at the specified Ethereum address.
func MakeEthNullAddressActor(av actorstypes.Version, addr address.Address) (*types.Actor, error) {
	actcid, ok := actors.GetActorCodeID(av, manifest.EthAccountKey)
	if !ok {
		return nil, xerrors.Errorf("failed to get EthAccount actor code ID for actors version %d", av)
	}

	act := &types.Actor{
		Code:    actcid,
		Head:    vm.EmptyObjectCid,
		Nonce:   0,
		Balance: big.Zero(),
		Address: &addr,
	}

	return act, nil
}

func SetupEthNullAddresses(ctx context.Context, st *state.StateTree, nv network.Version) ([]address.Address, error) {
	av, err := actorstypes.VersionForNetwork(nv)
	if err != nil {
		return nil, xerrors.Errorf("failed to resolve actors version for network version %d: %w", av, err)
	}

	if av < actorstypes.Version10 {
		// Not defined before version 10.
		return nil, nil
	}

	var ethAddresses []ethtypes.EthAddress
	for _, addr := range EthNullAddresses {
		a, err := ethtypes.ParseEthAddress(addr)
		if err != nil {
			return nil, xerrors.Errorf("failed to represent the 0x0 as an EthAddress: %w", err)
		}
		ethAddresses = append(ethAddresses, a)
	}

	initAct, err := st.GetActor(builtin.InitActorAddr)
	if err != nil {
		return nil, xerrors.Errorf("failed to load init actor: %w", err)
	}

	initState, err := init_.Load(adt.WrapStore(ctx, st.Store), initAct)
	if err != nil {
		return nil, xerrors.Errorf("failed to load init actor state: %w", err)
	}

	var ret []address.Address
	for _, ethAddr := range ethAddresses {
		// Place an EthAccount at the 0x0 Eth Null Address.
		f4Addr, err := ethAddr.ToFilecoinAddress()
		if err != nil {
			return nil, xerrors.Errorf("failed to compute Filecoin address for Eth addr 0x0: %w", err)
		}

		idAddr, err := initState.MapAddressToNewID(f4Addr)
		if err != nil {
			return nil, xerrors.Errorf("failed to map addr in init actor: %w", err)
		}

		actState, err := MakeEthNullAddressActor(av, f4Addr)
		if err != nil {
			return nil, xerrors.Errorf("failed to create EthAccount actor for null address: %w", err)
		}

		if err := st.SetActor(idAddr, actState); err != nil {
			return nil, xerrors.Errorf("failed to set Eth Null Address EthAccount actor state: %w", err)
		}

		ret = append(ret, idAddr)
	}

	initAct.Head, err = st.Store.Put(ctx, initState)
	if err != nil {
		return nil, xerrors.Errorf("failed to add init actor state to store: %w", err)
	}

	if err := st.SetActor(builtin.InitActorAddr, initAct); err != nil {
		return nil, xerrors.Errorf("failed to set updated state for init actor: %w", err)
	}

	return ret, nil
}
