package genesis

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

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

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

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

type SortedAddress []common.Address

func (a SortedAddress) Len() int           { return len(a) }
func (a SortedAddress) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a SortedAddress) Less(i, j int) bool { return bytes.Compare(a[i].Bytes(), a[j].Bytes()) < 0 }

func sortGenesisAllocs(allocs GenesisAlloc) []common.Address {
	var keys []common.Address
	for k := range allocs {
		keys = append(keys, k)
	}
	sort.Sort(SortedAddress(keys))
	return keys
}

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(engine exchain.Engine, chain chaindb.ChainDB) (err error) {
	blk := g.ToBlock()
	result, err := engine.ProcessPayload(blk)
	if err != nil {
		return err
	}
	if err := chain.SaveChainId(uint256.NewInt(g.ChainId)); err != nil {
		return err
	}
	if err := chain.SaveBlockData(blk, result.Receipts); err != nil {
		return err
	}
	return 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{
			Txs: make([]*nebulav1.Transaction, 0),
		},
	}
	accounts := sortGenesisAllocs(g.AllocInfo)
	for _, account := range accounts {
		info := g.AllocInfo[account]
		if len(info.SingerProxy) > 0 {
			tx := &nebulav1.Transaction{
				TxType: nebulav1.TxType_SignProxyTx,
				User:   account.String(),
				Nonce:  exchain.GetNonce().Bytes(),
				Proxy:  false,
				Tx: &nebulav1.Transaction_SignProxyTx{
					SignProxyTx: &nebulav1.SignProxyTransaction{
						SignerProxy: info.SingerProxy,
					},
				},
				Signature: nil,
			}
			blk.Transactions.Txs = append(blk.Transactions.Txs, tx)
		}
		for asset, wallet := range info.Assets {
			if wallet.Balance.Cmp(big.NewInt(0)) > 0 {
				tx := &nebulav1.Transaction{
					TxType: nebulav1.TxType_DepositTx,
					User:   account.String(),
					Nonce:  exchain.GetNonce().Bytes(),
					Proxy:  false,
					Tx: &nebulav1.Transaction_DepositTx{
						DepositTx: &nebulav1.DepositTransaction{
							SourceHash: common.Hash{}.Bytes(),
							User:       account.Bytes(),
							Coin:       []byte(asset),
							Amount:     wallet.Balance.Bytes(),
						},
					},
					Signature: nil,
				}
				blk.Transactions.Txs = append(blk.Transactions.Txs, tx)
			}
		}
	}

	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
}
