package genesis

import (
	"encoding/json"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/exchain/go-exchain/exchain/chaindb"
	nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
	"github.com/exchain/go-exchain/metadb"
	"github.com/golang/protobuf/proto"
	"github.com/holiman/uint256"
	"math/big"
	"os"
)

var (
	genesisSigner       = common.HexToAddress("0x5FbDB2315678afecb367f032d93F642f64180aa3")
	genesisSignerKey, _ = crypto.HexToECDSA("329cb309edf3b9d0a5d16a16df6393da089f00f555a3290873a9daabc9e39711")
)

type WalletInfo struct {
	Balance *big.Int `json:"balance"`
	Frozen  *big.Int `json:"frozen"`
}

type AssetsInfo map[string]WalletInfo

type GenesisAccount struct {
	SingerProxy []byte     `json:"signerProxy"`
	Assets      AssetsInfo `json:"assets"`
}

type GenesisBlock struct {
	ChainId   uint64       `json:"chainId"`
	Timestamp uint64       `json:"timestamp"`
	ExtraData []byte       `json:"extraData"`
	AllocInfo GenesisAlloc `json:"accounts"`
}

// GenesisAlloc specifies the initial state that is part of the genesis block.
type GenesisAlloc map[common.Address]GenesisAccount

func (ga *GenesisAlloc) UnmarshalJSON(data []byte) error {
	m := make(map[common.UnprefixedAddress]GenesisAccount)
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	*ga = make(GenesisAlloc)
	for addr, a := range m {
		(*ga)[common.Address(addr)] = a
	}
	return nil
}

func (g *GenesisBlock) Commit(db metadb.Database) (err error) {
	chain := chaindb.NewChainDB(db)
	blk := g.ToBlock()
	if err := chain.SaveChainId(uint256.NewInt(g.ChainId)); err != nil {
		return err
	}
	return chain.SaveBlockData(blk, nil)
}

func (g *GenesisBlock) ToBlock() *nebulav1.Block {
	blk := &nebulav1.Block{
		Header: &nebulav1.BlockHeader{
			Height:         0,
			ParentHash:     common.Hash{}.Bytes(),
			Timestamp:      g.Timestamp,
			Proposer:       genesisSigner.Bytes(),
			AppRoot:        common.Hash{}.Bytes(),
			L1Hash:         common.Hash{}.Bytes(),
			L1Height:       0,
			SequenceNumber: 0,
		},
		Transactions: &nebulav1.TransactionList{},
	}
	hash := BlockHash(blk)
	signature, _ := crypto.Sign(hash.Bytes(), genesisSignerKey)
	blk.Header.Signature = signature
	return blk
}

func (g *GenesisBlock) AddAccount(addr common.Address, account GenesisAccount) {
	g.AllocInfo[addr] = account
}

func NewGenesisBlock(genfile string) (*GenesisBlock, error) {
	data, err := os.ReadFile(genfile)
	if err != nil {
		return nil, err
	}
	var genblock GenesisBlock
	err = json.Unmarshal(data, &genblock)
	if err != nil {
		return nil, err
	}
	return &genblock, nil
}

func BlockHash(blk *nebulav1.Block) common.Hash {
	data := make([]byte, 0)
	data = append(data, uint256.NewInt(blk.Header.Height).Bytes()...)
	data = append(data, uint256.NewInt(blk.Header.Timestamp).Bytes()...)
	data = append(data, blk.Header.L1Hash...)
	data = append(data, uint256.NewInt(blk.Header.L1Height).Bytes()...)
	data = append(data, blk.Header.ParentHash...)
	data = append(data, blk.Header.AppRoot...)
	data = append(data, blk.Header.Proposer...)
	txdata, _ := proto.Marshal(blk.Transactions)
	data = append(data, txdata...)

	return crypto.Keccak256Hash(data)
}

func LoadGenesisAllocs(allocsPath string) (GenesisAlloc, error) {
	f, err := os.OpenFile(allocsPath, os.O_RDONLY, 0644)
	if err != nil {
		return nil, fmt.Errorf("failed to open forge allocs %q: %w", allocsPath, err)
	}
	defer f.Close()
	var out = make(GenesisAlloc)
	if err := json.NewDecoder(f).Decode(&out); err != nil {
		return nil, fmt.Errorf("failed to json-decode genesis allocs %q: %w", allocsPath, err)
	}
	return out, nil
}
