package testnet

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math/big"
	"os"
	"strings"
	"time"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
	"github.com/OffchainLabs/prysm/v7/cmd/flags"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/container/trie"
	"github.com/OffchainLabs/prysm/v7/io/file"
	ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v7/runtime/interop"
	"github.com/OffchainLabs/prysm/v7/runtime/version"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/rpc"
	"github.com/ghodss/yaml"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
)

var (
	generateGenesisStateFlags = struct {
		DepositJsonFile    string
		ChainConfigFile    string
		NumValidators      uint64
		GenesisTime        uint64
		GenesisTimeDelay   uint64
		OutputSSZ          string
		OutputJSON         string
		OutputYaml         string
		ForkName           string
		OverrideEth1Data   bool
		ExecutionEndpoint  string
		GethGenesisJsonIn  string
		GethGenesisJsonOut string
	}{}
	log           = logrus.WithField("prefix", "genesis")
	outputSSZFlag = &cli.StringFlag{
		Name:        "output-ssz",
		Destination: &generateGenesisStateFlags.OutputSSZ,
		Usage:       "Output filename of the SSZ marshaling of the generated genesis state",
		Value:       "",
	}
	outputYamlFlag = &cli.StringFlag{
		Name:        "output-yaml",
		Destination: &generateGenesisStateFlags.OutputYaml,
		Usage:       "Output filename of the YAML marshaling of the generated genesis state",
		Value:       "",
	}
	outputJsonFlag = &cli.StringFlag{
		Name:        "output-json",
		Destination: &generateGenesisStateFlags.OutputJSON,
		Usage:       "Output filename of the JSON marshaling of the generated genesis state",
		Value:       "",
	}
	generateGenesisStateCmd = &cli.Command{
		Name:  "generate-genesis",
		Usage: "Generate a beacon chain genesis state",
		Action: func(cliCtx *cli.Context) error {
			if err := cliActionGenerateGenesisState(cliCtx); err != nil {
				log.WithError(err).Fatal("Could not generate beacon chain genesis state")
			}
			return nil
		},
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:        "chain-config-file",
				Destination: &generateGenesisStateFlags.ChainConfigFile,
				Usage:       "The path to a YAML file with chain config values",
			},
			&cli.StringFlag{
				Name:        "deposit-json-file",
				Destination: &generateGenesisStateFlags.DepositJsonFile,
				Usage:       "Path to deposit_data.json file generated by the staking-deposit-cli tool for optionally specifying validators in genesis state",
			},
			&cli.Uint64Flag{
				Name:        "num-validators",
				Usage:       "Number of validators to deterministically generate in the genesis state",
				Destination: &generateGenesisStateFlags.NumValidators,
				Required:    true,
			},
			&cli.Uint64Flag{
				Name:        "genesis-time",
				Destination: &generateGenesisStateFlags.GenesisTime,
				Usage:       "Unix timestamp seconds used as the genesis time in the genesis state. If unset, defaults to now()",
			},
			&cli.Uint64Flag{
				Name:        "genesis-time-delay",
				Destination: &generateGenesisStateFlags.GenesisTimeDelay,
				Usage:       "Delay genesis time by N seconds",
			},
			&cli.BoolFlag{
				Name:        "override-eth1data",
				Destination: &generateGenesisStateFlags.OverrideEth1Data,
				Usage:       "Overrides Eth1Data with values from execution client. If unset, defaults to false",
				Value:       false,
			},
			&cli.StringFlag{
				Name:        "geth-genesis-json-in",
				Destination: &generateGenesisStateFlags.GethGenesisJsonIn,
				Usage:       "Path to a \"genesis.json\" file, containing a json representation of Geth's core.Genesis",
			},
			&cli.StringFlag{
				Name:        "geth-genesis-json-out",
				Destination: &generateGenesisStateFlags.GethGenesisJsonOut,
				Usage:       "Path to write generated \"genesis.json\" file, containing a json representation of Geth's core.Genesis",
			},
			&cli.StringFlag{
				Name:        "execution-endpoint",
				Destination: &generateGenesisStateFlags.ExecutionEndpoint,
				Usage:       "Endpoint to preferred execution client. If unset, defaults to Geth",
				Value:       "http://localhost:8545",
			},
			flags.EnumValue{
				Name:        "fork",
				Usage:       fmt.Sprintf("Name of the BeaconState schema to use in output encoding [%s]", strings.Join(versionNames(), ",")),
				Enum:        versionNames(),
				Value:       versionNames()[0],
				Destination: &generateGenesisStateFlags.ForkName,
			}.GenericFlag(),
			outputSSZFlag,
			outputYamlFlag,
			outputJsonFlag,
		},
	}
)

func versionNames() []string {
	enum := version.All()
	names := make([]string, len(enum))
	for i := range enum {
		names[i] = version.String(enum[i])
	}
	return names
}

// Represents a json object of hex string and uint64 values for
// validators on Ethereum. This file can be generated using the official staking-deposit-cli.
type depositDataJSON struct {
	PubKey                string `json:"pubkey"`
	Amount                uint64 `json:"amount"`
	WithdrawalCredentials string `json:"withdrawal_credentials"`
	DepositDataRoot       string `json:"deposit_data_root"`
	Signature             string `json:"signature"`
}

func cliActionGenerateGenesisState(cliCtx *cli.Context) error {
	outputJson := generateGenesisStateFlags.OutputJSON
	outputYaml := generateGenesisStateFlags.OutputYaml
	outputSSZ := generateGenesisStateFlags.OutputSSZ
	noOutputFlag := outputSSZ == "" && outputJson == "" && outputYaml == ""
	if noOutputFlag {
		return fmt.Errorf(
			"no %s, %s, %s flag(s) specified. At least one is required",
			outputJsonFlag.Name,
			outputYamlFlag.Name,
			outputSSZFlag.Name,
		)
	}
	if err := setGlobalParams(); err != nil {
		return fmt.Errorf("could not set config params: %w", err)
	}
	st, err := generateGenesis(cliCtx.Context)
	if err != nil {
		return fmt.Errorf("could not generate genesis state: %w", err)
	}

	if outputJson != "" {
		if err := writeToOutputFile(outputJson, st, json.Marshal); err != nil {
			return err
		}
	}
	if outputYaml != "" {
		if err := writeToOutputFile(outputYaml, st, yaml.Marshal); err != nil {
			return err
		}
	}
	if outputSSZ != "" {
		type MinimumSSZMarshal interface {
			MarshalSSZ() ([]byte, error)
		}
		marshalFn := func(o any) ([]byte, error) {
			marshaler, ok := o.(MinimumSSZMarshal)
			if !ok {
				return nil, errors.New("not a marshaler")
			}
			return marshaler.MarshalSSZ()
		}
		if err := writeToOutputFile(outputSSZ, st, marshalFn); err != nil {
			return err
		}
	}
	log.Info("Command completed")
	return nil
}

func setGlobalParams() error {
	chainConfigFile := generateGenesisStateFlags.ChainConfigFile
	if chainConfigFile != "" {
		log.Infof("Specified a chain config file: %s", chainConfigFile)
		return params.LoadChainConfigFile(chainConfigFile, nil)
	}
	return errors.New("No chain config file was provided. Use `--chain-config-file` to provide a chain config.")
}

func generateGenesis(ctx context.Context) (state.BeaconState, error) {
	f := &generateGenesisStateFlags
	if f.GenesisTime == 0 {
		f.GenesisTime = uint64(time.Now().Unix())
		log.Info("No genesis time specified, defaulting to now()")
	}
	log.Infof("Delaying genesis %v by %v seconds", f.GenesisTime, f.GenesisTimeDelay)
	f.GenesisTime += f.GenesisTimeDelay
	log.Infof("Genesis is now %v", f.GenesisTime)

	v, err := version.FromString(f.ForkName)
	if err != nil {
		return nil, err
	}
	opts := make([]interop.PremineGenesisOpt, 0)
	nv := f.NumValidators
	if f.DepositJsonFile != "" {
		expanded, err := file.ExpandPath(f.DepositJsonFile)
		if err != nil {
			return nil, err
		}
		log.Printf("Reading deposits from JSON at %s", expanded)
		b, err := os.ReadFile(expanded) // #nosec G304
		if err != nil {
			return nil, err
		}
		roots, dds, err := depositEntriesFromJSON(b)
		if err != nil {
			return nil, err
		}
		opts = append(opts, interop.WithDepositData(dds, roots))
	} else if nv == 0 {
		return nil, fmt.Errorf(
			"expected --num-validators > 0 or --deposit-json-file to have been provided",
		)
	}

	gen := &core.Genesis{}
	if f.GethGenesisJsonIn != "" {
		gbytes, err := os.ReadFile(f.GethGenesisJsonIn) // #nosec G304
		if err != nil {
			return nil, errors.Wrapf(err, "failed to read %s", f.GethGenesisJsonIn)
		}
		if err := json.Unmarshal(gbytes, gen); err != nil {
			return nil, err
		}
		// set timestamps for genesis and shanghai fork
		gen.Timestamp = f.GenesisTime
		genesis := time.Unix(int64(f.GenesisTime), 0)
		gen.Config.ShanghaiTime = interop.GethShanghaiTime(genesis, params.BeaconConfig())
		gen.Config.CancunTime = interop.GethCancunTime(genesis, params.BeaconConfig())
		gen.Config.PragueTime = interop.GethPragueTime(genesis, params.BeaconConfig())
		gen.Config.OsakaTime = interop.GethOsakaTime(genesis, params.BeaconConfig())

		fields := logrus.Fields{}
		if gen.Config.ShanghaiTime != nil {
			fields["shanghai"] = fmt.Sprintf("%d", *gen.Config.ShanghaiTime)
		}
		if gen.Config.CancunTime != nil {
			fields["cancun"] = fmt.Sprintf("%d", *gen.Config.CancunTime)
		}
		if gen.Config.PragueTime != nil {
			fields["prague"] = fmt.Sprintf("%d", *gen.Config.PragueTime)
		}
		if gen.Config.OsakaTime != nil {
			fields["osaka"] = fmt.Sprintf("%d", *gen.Config.OsakaTime)
		}
		log.WithFields(fields).Info("Setting fork geth times")
		if v > version.Altair {
			// set ttd to zero so EL goes post-merge immediately
			gen.Config.TerminalTotalDifficulty = big.NewInt(0)
			if gen.BaseFee == nil {
				return nil, errors.New("baseFeePerGas must be set in genesis.json for Post-Merge networks (after Altair)")
			}
		} else {
			if gen.BaseFee == nil {
				gen.BaseFee = big.NewInt(1000000000) // 1 Gwei default
				log.WithField("baseFeePerGas", "1000000000").Warn("BaseFeePerGas not specified in genesis.json, using default value of 1 Gwei")
			}
		}
	} else {
		gen = interop.GethTestnetGenesis(time.Unix(int64(f.GenesisTime), 0), params.BeaconConfig())
	}

	if f.GethGenesisJsonOut != "" {
		gbytes, err := json.MarshalIndent(gen, "", "\t")
		if err != nil {
			return nil, err
		}
		if err := os.WriteFile(f.GethGenesisJsonOut, gbytes, 0600); err != nil {
			return nil, errors.Wrapf(err, "failed to write %s", f.GethGenesisJsonOut)
		}
	}

	gb := gen.ToBlock()

	// TODO: expose the PregenesisCreds option with a cli flag - for now defaulting to no withdrawal credentials at genesis
	genesisState, err := interop.NewPreminedGenesis(ctx, time.Unix(int64(f.GenesisTime), 0), nv, 0, v, gb, opts...)
	if err != nil {
		return nil, err
	}

	if f.OverrideEth1Data {
		log.Print("Overriding Eth1Data with data from execution client")
		conn, err := rpc.Dial(generateGenesisStateFlags.ExecutionEndpoint)
		if err != nil {
			return nil, errors.Wrapf(
				err,
				"could not dial %s please make sure you are running your execution client",
				generateGenesisStateFlags.ExecutionEndpoint)
		}
		client := ethclient.NewClient(conn)
		header, err := client.HeaderByNumber(ctx, big.NewInt(0))
		if err != nil {
			return nil, errors.Wrap(err, "could not get header by number")
		}
		t, err := trie.NewTrie(params.BeaconConfig().DepositContractTreeDepth)
		if err != nil {
			return nil, errors.Wrap(err, "could not create deposit tree")
		}
		depositRoot, err := t.HashTreeRoot()
		if err != nil {
			return nil, errors.Wrap(err, "could not get hash tree root")
		}
		e1d := &ethpb.Eth1Data{
			DepositRoot:  depositRoot[:],
			DepositCount: 0,
			BlockHash:    header.Hash().Bytes(),
		}
		if err := genesisState.SetEth1Data(e1d); err != nil {
			return nil, err
		}
		if err := genesisState.SetEth1DepositIndex(0); err != nil {
			return nil, err
		}
	}

	return genesisState, err
}

func depositEntriesFromJSON(enc []byte) ([][]byte, []*ethpb.Deposit_Data, error) {
	var depositJSON []*depositDataJSON
	if err := json.Unmarshal(enc, &depositJSON); err != nil {
		return nil, nil, err
	}
	dds := make([]*ethpb.Deposit_Data, len(depositJSON))
	roots := make([][]byte, len(depositJSON))
	for i, val := range depositJSON {
		root, data, err := depositJSONToDepositData(val)
		if err != nil {
			return nil, nil, err
		}
		dds[i] = data
		roots[i] = root
	}
	return roots, dds, nil
}

func depositJSONToDepositData(input *depositDataJSON) ([]byte, *ethpb.Deposit_Data, error) {
	root, err := hex.DecodeString(strings.TrimPrefix(input.DepositDataRoot, "0x"))
	if err != nil {
		return nil, nil, err
	}
	pk, err := hex.DecodeString(strings.TrimPrefix(input.PubKey, "0x"))
	if err != nil {
		return nil, nil, err
	}
	creds, err := hex.DecodeString(strings.TrimPrefix(input.WithdrawalCredentials, "0x"))
	if err != nil {
		return nil, nil, err
	}
	sig, err := hex.DecodeString(strings.TrimPrefix(input.Signature, "0x"))
	if err != nil {
		return nil, nil, err
	}
	return root, &ethpb.Deposit_Data{
		PublicKey:             pk,
		WithdrawalCredentials: creds,
		Amount:                input.Amount,
		Signature:             sig,
	}, nil
}

func writeToOutputFile(
	fPath string,
	data any,
	marshalFn func(o any) ([]byte, error),
) error {
	encoded, err := marshalFn(data)
	if err != nil {
		return err
	}
	if err := file.WriteFile(fPath, encoded); err != nil {
		return err
	}
	log.Printf("Done writing genesis state to %s", fPath)
	return nil
}
