package interop

import (
	"math"
	"math/big"
	"time"

	clparams "github.com/OffchainLabs/prysm/v6/config/params"
	"github.com/OffchainLabs/prysm/v6/time/slots"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/params"
)

// defaultMinerAddress is used to send deposits and test transactions in the e2e test.
// This account is given a large initial balance in the genesis block in test setups.
const defaultMinerAddress = "0x878705ba3f8bc32fcf7f4caa1a35e72af65cf766"
const defaultTestChainId int64 = 1337
const defaultCoinbase = "0x0000000000000000000000000000000000000000"
const defaultDifficulty = "1"
const defaultMixhash = "0x0000000000000000000000000000000000000000000000000000000000000000"
const defaultParenthash = "0x0000000000000000000000000000000000000000000000000000000000000000"
const defaultMinerBalance = "100000000000000000000000000000"

// DepositContractCode is the compiled deposit contract code, via https://github.com/protolambda/merge-genesis-tools
// This is embedded into genesis so that we can start the chain at a merge block.
// const DepositContractCode = "[encoded data, 12718 characters]"
const DepositContractCode = "[encoded data, 12156 characters]"

// DefaultDepositContractStorage represents the empty deposit trie used by the deposit contract.
// For details see https://github.com/protolambda/merge-genesis-tools
var DefaultDepositContractStorage = map[string]string{
	"0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
	"0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
	"0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c",
	"0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c",
	"0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
	"0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
	"0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c",
	"0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193",
	"0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
	"0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
	"0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
	"0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
	"0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
	"0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
	"0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
	"0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
	"0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
	"0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
	"0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
	"0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
	"0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
	"0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
	"0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
	"0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
	"0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
	"0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
	"0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
	"0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
	"0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
	"0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
	"0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
}

var bigz = big.NewInt(0)
var minerBalance = big.NewInt(0)

// GethShanghaiTime calculates the absolute time of the shanghai (aka capella) fork block
// by adding the relative time of the capella the fork epoch to the given genesis timestamp.
func GethShanghaiTime(genesisTime time.Time, cfg *clparams.BeaconChainConfig) *uint64 {
	var shanghaiTime *uint64
	if cfg.CapellaForkEpoch != math.MaxUint64 {
		startSlot, err := slots.EpochStart(cfg.CapellaForkEpoch)
		if err == nil {
			startTime := slots.UnsafeStartTime(genesisTime, startSlot)
			newTime := uint64(startTime.Unix())
			shanghaiTime = &newTime
		}
	}
	return shanghaiTime
}

// GethCancunTime calculates the absolute time of the cancun (aka deneb) fork block
// by adding the relative time of the capella the fork epoch to the given genesis timestamp.
func GethCancunTime(genesisTime time.Time, cfg *clparams.BeaconChainConfig) *uint64 {
	var cancunTime *uint64
	if cfg.DenebForkEpoch != math.MaxUint64 {
		startSlot, err := slots.EpochStart(cfg.DenebForkEpoch)
		if err == nil {
			startTime := slots.UnsafeStartTime(genesisTime, startSlot)
			newTime := uint64(startTime.Unix())
			cancunTime = &newTime
		}
	}
	return cancunTime
}

// GethPragueTime calculates the absolute time of the prague (aka electra) fork block
// by adding the relative time of the capella the fork epoch to the given genesis timestamp.
func GethPragueTime(genesisTime time.Time, cfg *clparams.BeaconChainConfig) *uint64 {
	var pragueTime *uint64
	if cfg.ElectraForkEpoch != math.MaxUint64 {
		startSlot, err := slots.EpochStart(cfg.ElectraForkEpoch)
		if err == nil {
			startTime := slots.UnsafeStartTime(genesisTime, startSlot)
			newTime := uint64(startTime.Unix())
			pragueTime = &newTime
		}
	}
	return pragueTime
}

// GethTestnetGenesis creates a genesis.json for eth1 clients with a set of defaults suitable for ephemeral testnets,
// like in an e2e test. The parameters are minimal but the full value is returned unmarshaled so that it can be
// customized as desired.
func GethTestnetGenesis(genesis time.Time, cfg *clparams.BeaconChainConfig) *core.Genesis {
	genesisTime := uint64(genesis.Unix())
	shanghaiTime := GethShanghaiTime(genesis, cfg)
	if cfg.CapellaForkEpoch == 0 {
		shanghaiTime = &genesisTime
	}
	cancunTime := GethCancunTime(genesis, cfg)
	if cfg.DenebForkEpoch == 0 {
		cancunTime = &genesisTime
	}
	pragueTime := GethPragueTime(genesis, cfg)
	if cfg.ElectraForkEpoch == 0 {
		pragueTime = &genesisTime
	}
	cc := &params.ChainConfig{
		ChainID:                 big.NewInt(defaultTestChainId),
		HomesteadBlock:          bigz,
		DAOForkBlock:            bigz,
		EIP150Block:             bigz,
		EIP155Block:             bigz,
		EIP158Block:             bigz,
		ByzantiumBlock:          bigz,
		ConstantinopleBlock:     bigz,
		PetersburgBlock:         bigz,
		IstanbulBlock:           bigz,
		MuirGlacierBlock:        bigz,
		BerlinBlock:             bigz,
		LondonBlock:             bigz,
		ArrowGlacierBlock:       bigz,
		GrayGlacierBlock:        bigz,
		MergeNetsplitBlock:      bigz,
		TerminalTotalDifficulty: bigz,
		ShanghaiTime:            shanghaiTime,
		CancunTime:              cancunTime,
		PragueTime:              pragueTime,
		DepositContractAddress:  common.HexToAddress(cfg.DepositContractAddress),
		BlobScheduleConfig: &params.BlobScheduleConfig{
			Cancun: &params.BlobConfig{
				Target:         3,
				Max:            6,
				UpdateFraction: 3338477,
			},
			Prague: &params.BlobConfig{
				Target:         6,
				Max:            9,
				UpdateFraction: 5007716,
			},
		},
	}
	da := defaultDepositContractAllocation(cfg.DepositContractAddress)
	ma := minerAllocation()
	return &core.Genesis{
		Config:     cc,
		Nonce:      0, // overridden for authorized signer votes in clique, so we should leave it empty?
		Timestamp:  genesisTime,
		GasLimit:   cfg.DefaultBuilderGasLimit,
		Difficulty: common.HexToHash(defaultDifficulty).Big(),
		Mixhash:    common.HexToHash(defaultMixhash),
		Coinbase:   common.HexToAddress(defaultCoinbase),
		Alloc: types.GenesisAlloc{
			da.Address: da.Account,
			ma.Address: ma.Account,
		},
		ParentHash: common.HexToHash(defaultParenthash),
	}
}

type depositAllocation struct {
	Address common.Address
	Account types.Account
}

func minerAllocation() depositAllocation {
	return depositAllocation{
		Address: common.HexToAddress(defaultMinerAddress),
		Account: types.Account{
			Balance: minerBalance,
		},
	}
}

func defaultDepositContractAllocation(contractAddress string) depositAllocation {
	s := make(map[common.Hash]common.Hash)
	for k, v := range DefaultDepositContractStorage {
		s[common.HexToHash(k)] = common.HexToHash(v)
	}
	codeBytes, err := hexutil.Decode(DepositContractCode)
	if err != nil {
		panic(err) // lint:nopanic -- The deposit contract code is hardcoded and checked in tests.
	}
	return depositAllocation{
		Address: common.HexToAddress(contractAddress),
		Account: types.Account{
			Code:    codeBytes,
			Storage: s,
			Balance: bigz,
			Nonce:   deterministicNonce(0),
		},
	}
}

func deterministicNonce(i uint64) uint64 {
	return math.MaxUint64/2 + i
}

func init() {
	err := minerBalance.UnmarshalText([]byte(defaultMinerBalance))
	if err != nil {
		panic(err)
	}
}
