package testkit

import (
	"bytes"
	"context"
	"fmt"
	mbig "math/big"
	"time"

	"github.com/google/uuid"
	"github.com/libp2p/go-libp2p-core/peer"
	ma "github.com/multiformats/go-multiaddr"

	"github.com/filecoin-project/go-state-types/big"

	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/gen"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/genesis"
	"github.com/filecoin-project/lotus/node"
	"github.com/filecoin-project/lotus/node/modules"
	modtest "github.com/filecoin-project/lotus/node/modules/testing"
	"github.com/filecoin-project/lotus/node/repo"
)

// Bootstrapper is a special kind of process that produces a genesis block with
// the initial wallet balances and preseals for all enlisted miners and clients.
type Bootstrapper struct {
	*LotusNode

	t *TestEnvironment
}

func PrepareBootstrapper(t *TestEnvironment) (*Bootstrapper, error) {
	var (
		clients = t.IntParam("clients")
		miners  = t.IntParam("miners")
		nodes   = clients + miners
	)

	ctx, cancel := context.WithTimeout(context.Background(), PrepareNodeTimeout)
	defer cancel()

	pubsubTracerMaddr, err := GetPubsubTracerMaddr(ctx, t)
	if err != nil {
		return nil, err
	}

	randomBeaconOpt, err := GetRandomBeaconOpts(ctx, t)
	if err != nil {
		return nil, err
	}

	// the first duty of the boostrapper is to construct the genesis block
	// first collect all client and miner balances to assign initial funds
	balances, err := WaitForBalances(t, ctx, nodes)
	if err != nil {
		return nil, err
	}

	totalBalance := big.Zero()
	for _, b := range balances {
		totalBalance = big.Add(filToAttoFil(b.Balance), totalBalance)
	}

	totalBalanceFil := attoFilToFil(totalBalance)
	t.RecordMessage("TOTAL BALANCE: %s AttoFIL (%s FIL)", totalBalance, totalBalanceFil)
	if max := types.TotalFilecoinInt; totalBalanceFil.GreaterThanEqual(max) {
		panic(fmt.Sprintf("total sum of balances is greater than max Filecoin ever; sum=%s, max=%s", totalBalance, max))
	}

	// then collect all preseals from miners
	preseals, err := CollectPreseals(t, ctx, miners)
	if err != nil {
		return nil, err
	}

	// now construct the genesis block
	var genesisActors []genesis.Actor
	var genesisMiners []genesis.Miner

	for _, bm := range balances {
		balance := filToAttoFil(bm.Balance)
		t.RecordMessage("balance assigned to actor %s: %s AttoFIL", bm.Addr, balance)
		genesisActors = append(genesisActors,
			genesis.Actor{
				Type:    genesis.TAccount,
				Balance: balance,
				Meta:    (&genesis.AccountMeta{Owner: bm.Addr}).ActorMeta(),
			})
	}

	for _, pm := range preseals {
		genesisMiners = append(genesisMiners, pm.Miner)
	}

	genesisTemplate := genesis.Template{
		Accounts:         genesisActors,
		Miners:           genesisMiners,
		Timestamp:        uint64(time.Now().Unix()) - uint64(t.IntParam("genesis_timestamp_offset")),
		VerifregRootKey:  gen.DefaultVerifregRootkeyActor,
		RemainderAccount: gen.DefaultRemainderAccountActor,
		NetworkName:      "testground-local-" + uuid.New().String(),
	}

	// dump the genesis block
	// var jsonBuf bytes.Buffer
	// jsonEnc := json.NewEncoder(&jsonBuf)
	// err := jsonEnc.Encode(genesisTemplate)
	// if err != nil {
	// 	panic(err)
	// }
	// runenv.RecordMessage(fmt.Sprintf("Genesis template: %s", string(jsonBuf.Bytes())))

	// this is horrendously disgusting, we use this contraption to side effect the construction
	// of the genesis block in the buffer -- yes, a side effect of dependency injection.
	// I remember when software was straightforward...
	var genesisBuffer bytes.Buffer

	bootstrapperIP := t.NetClient.MustGetDataNetworkIP().String()

	n := &LotusNode{}
	r := repo.NewMemory(nil)
	stop, err := node.New(context.Background(),
		node.FullAPI(&n.FullApi),
		node.Base(),
		node.Repo(r),
		node.Override(new(modules.Genesis), modtest.MakeGenesisMem(&genesisBuffer, genesisTemplate)),
		withApiEndpoint(fmt.Sprintf("/ip4/0.0.0.0/tcp/%s", t.PortNumber("node_rpc", "0"))),
		withListenAddress(bootstrapperIP),
		withBootstrapper(nil),
		withPubsubConfig(true, pubsubTracerMaddr),
		randomBeaconOpt,
	)
	if err != nil {
		return nil, err
	}
	n.StopFn = stop

	var bootstrapperAddr ma.Multiaddr

	bootstrapperAddrs, err := n.FullApi.NetAddrsListen(ctx)
	if err != nil {
		stop(context.TODO())
		return nil, err
	}
	for _, a := range bootstrapperAddrs.Addrs {
		ip, err := a.ValueForProtocol(ma.P_IP4)
		if err != nil {
			continue
		}
		if ip != bootstrapperIP {
			continue
		}
		addrs, err := peer.AddrInfoToP2pAddrs(&peer.AddrInfo{
			ID:    bootstrapperAddrs.ID,
			Addrs: []ma.Multiaddr{a},
		})
		if err != nil {
			panic(err)
		}
		bootstrapperAddr = addrs[0]
		break
	}

	if bootstrapperAddr == nil {
		panic("failed to determine bootstrapper address")
	}

	genesisMsg := &GenesisMsg{
		Genesis:      genesisBuffer.Bytes(),
		Bootstrapper: bootstrapperAddr.Bytes(),
	}
	t.SyncClient.MustPublish(ctx, GenesisTopic, genesisMsg)

	t.RecordMessage("waiting for all nodes to be ready")
	t.SyncClient.MustSignalAndWait(ctx, StateReady, t.TestInstanceCount)

	return &Bootstrapper{n, t}, nil
}

// RunDefault runs a default bootstrapper.
func (b *Bootstrapper) RunDefault() error {
	b.t.RecordMessage("running bootstrapper")
	ctx := context.Background()
	b.t.SyncClient.MustSignalAndWait(ctx, StateDone, b.t.TestInstanceCount)
	return nil
}

// filToAttoFil converts a fractional filecoin value into AttoFIL, rounding if necessary
func filToAttoFil(f float64) big.Int {
	a := mbig.NewFloat(f)
	a.Mul(a, mbig.NewFloat(float64(build.FilecoinPrecision)))
	i, _ := a.Int(nil)
	return big.Int{Int: i}
}

func attoFilToFil(atto big.Int) big.Int {
	i := big.NewInt(0)
	i.Add(i.Int, atto.Int)
	i.Div(i.Int, big.NewIntUnsigned(build.FilecoinPrecision).Int)
	return i
}