package multisend

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"log"
	"math/big"
	"net/http"
	"strconv"
	"sync/atomic"
	"time"

	"sync"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/google/uuid"
	"github.com/gorilla/mux"
	hdwallet "github.com/miguelmota/go-ethereum-hdwallet"
	"github.com/rs/cors"
	"github.com/shopspring/decimal"
)

var mnemonic = "matter someone fee garlic final police during vapor stool cargo snake dove"

var processMap sync.Map
var Running int32 = 0

var systemFromAddr string

func init() {

	wallet, err := hdwallet.NewFromMnemonic(mnemonic)
	if err != nil {
		log.Fatal(err)
	}

	path := hdwallet.MustParseDerivationPath(fmt.Sprintf("m/44'/60'/0'/0/%d", 0))
	account, err := wallet.Derive(path, false)
	if err != nil {
		panic(err)
	}

	systemFromAddr = account.Address.Hex()
}

func GetSendRecord(id uuid.UUID) (SendRecord, bool) {

	if vAsInterface, ok := processMap.Load(id); ok {

		if sendR, ok := vAsInterface.(SendRecord); ok {
			return sendR, true
		}
	}

	// recordAsJson, err := redisCli.Get(context.Background(), id.String()).Result()

	// if err != nil {
	// 	panic(err)
	// }

	// fmt.Printf("recordAsJson: %s \n", recordAsJson)

	// sendRec := SendRecord{}

	// if err := json.Unmarshal([]byte(recordAsJson), sendRec); err != nil {
	// 	panic(err)
	// }

	return SendRecord{}, false
}

func SetSendRecord(id uuid.UUID, value SendRecord) {
	processMap.Store(id, value)

	sent := 0

	for _, v := range value.SendRecord {
		sent += v.TxNum
	}

	// if sent == int(value.TotalConsTx) {
	// 	recoradAsJson, err := json.Marshal(value)
	// 	if err != nil {
	// 		panic(err)
	// 	}
	// 	redisCli.Set(context.Background(), id.String(), recoradAsJson, time.Hour*24)
	// }
}

type SendRecord struct {
	Percent     float64     `json:"percent"`
	TotalConsTx int64       `json:"total_cons_tx"`
	SendRecord  []BatchSend `json:"send_record"`
}

type ConsTxWithBatchHash struct {
	ConsTxHash   []byte `json:"cons_tx_hash"`
	BatchTxsHash []byte `json:"batch_txs_hash"`
}

type ConsTxWithBatchHashAsHex struct {
	ConsTxHash   string   `json:"cons_tx_hash"`
	BatchTxsHash []string `json:"batch_txs_hash"`
}

type BatchSend struct {
	SendToRedisBeginTime int64                 `json:"send_to_redis_begin_time"`
	SendTxsEndTime       int64                 `json:"send_txs_end_time"`
	TxNum                int                   `json:"cons_tx_num"`
	BeginOriginalTx      common.Hash           `json:"begin_original_tx"`
	EndOriginalTx        common.Hash           `json:"end_original_tx"`
	ConsTxWithBatchHash  []ConsTxWithBatchHash `json:"con_tx_with_batch_hash""`
}

type WebServicer struct {
	transactor *Transactor
	cli        Client
}

const MaxToAddrsNum = 1000
const MaxTxCount = 5 * 1000 * 1000
const MaxRequestAmount = MaxTxCount * 10

func (web *WebServicer) ParamHandler(w http.ResponseWriter, r *http.Request) {

	wallet, err := hdwallet.NewFromMnemonic(mnemonic)
	if err != nil {
		log.Fatal(err)
	}

	resp := struct {
		From                 string   `json:"from"`
		ToAddrs              []string `json:"to_addrs"`
		MaxToAddrsNum        int64    `json:"max_to_addrs_num"`
		MaxRequestAmount     int64    `json:"max_request_amount"`
		MaxTxCount           int64    `json:"max_tx_count"`
		SuggestRequestAmount int64    `json:"suggest_request_amount"`
		SuggestTxCount       int64    `json:"suggest_tx_count"`
		SuggestEveryTxAmount int64    `json:"suggest_every_tx_amount"`
	}{}

	resp.MaxToAddrsNum = MaxToAddrsNum
	resp.MaxTxCount = MaxTxCount
	resp.MaxRequestAmount = MaxRequestAmount
	resp.SuggestRequestAmount = 1000000
	resp.SuggestTxCount = 1000000
	resp.SuggestEveryTxAmount = 1

	for i := 1; i <= 1001; i++ {

		path := hdwallet.MustParseDerivationPath(fmt.Sprintf("m/44'/60'/0'/0/%d", i))
		account, err := wallet.Derive(path, false)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		if i == 1 {
			resp.From = account.Address.Hex()
			continue
		}

		resp.ToAddrs = append(resp.ToAddrs, account.Address.Hex())

	}

	respAsJson, err := json.Marshal(resp)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	w.Write(respAsJson)
}

func (web *WebServicer) GetTreeHandler(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)
	uuidStr := vars["uuid"]

	id, err := uuid.Parse(uuidStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	consTxWithBatchHashAsHex := []ConsTxWithBatchHashAsHex{}
	if record, ok := GetSendRecord(id); ok {

		for _, v := range record.SendRecord {
			for _, v1 := range v.ConsTxWithBatchHash {

				batchTxsAsHex := []string{}

				for i := 0; ; i++ {

					if len(v1.BatchTxsHash) < 32 {
						break
					}
					batchTxsAsHex = append(batchTxsAsHex, fmt.Sprintf("%x", v1.BatchTxsHash[:32]))
					v1.BatchTxsHash = v1.BatchTxsHash[32:]
				}

				consTxWithBatchHashAsHex = append(consTxWithBatchHashAsHex,
					ConsTxWithBatchHashAsHex{
						ConsTxHash:   fmt.Sprintf("%x", v1.ConsTxHash),
						BatchTxsHash: batchTxsAsHex,
					})
			}
		}

		recordAsJon, err := json.Marshal(consTxWithBatchHashAsHex)

		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write(recordAsJon)
		return

	}

	// record := redisCli.Get(context.Background(), id.String()).Scan()

	if err != nil {
		http.Error(w, fmt.Sprintf("can not find the uuid: %s ", id.String()), http.StatusInternalServerError)
		return
	}

}

type resp struct {
	HashList []string `json:"hash_list"`
	Total    uint64   `json:"total"`
}

func (web *WebServicer) GetTxsList(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)
	hashStr := vars["hash"]

	startAsStr := r.URL.Query().Get("start")
	numStr := r.URL.Query().Get("num")

	startAsInt, err := strconv.Atoi(startAsStr)
	if err != nil {
		startAsInt = 0
	}

	numInt, err := strconv.Atoi(numStr)
	if err != nil {
		numInt = 10
	}

	if startAsInt != 0 {
		startAsInt = startAsInt - 1
	}

	fmt.Printf("startAsStr: %s offsetStr: %s startAs: %d offset: %d \n", startAsStr, numStr, startAsInt, numInt)

	// hashAsBytes, err := base64.StdEncoding.DecodeString(hashStr)

	// if err != nil {
	// 	http.Error(w, err.Error(), http.StatusBadRequest)
	// 	return
	// }

	//total, err := redisCli.LLen(context.Background(), fmt.Sprintf("%x", hashAsBytes)).Uint64()
	total, err := redisCli.LLen(context.Background(), hashStr).Uint64()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	//res, err := redisCli.LRange(context.Background(), fmt.Sprintf("%x", hashAsBytes), int64(startAsInt*numInt), int64((startAsInt+1)*numInt)-1).Result()
	res, err := redisCli.LRange(context.Background(), hashStr, int64(startAsInt*numInt), int64((startAsInt+1)*numInt)-1).Result()

	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	fmt.Printf("hashStr: %s  len(hashStr): %d int64(startAsInt*numInt): %d  int64((startAsInt+1)*numInt): %d res: %v \n", hashStr, len(hashStr), int64(startAsInt*numInt), int64((startAsInt+1)*numInt-1), res)

	resp := resp{
		HashList: res,
		Total:    total,
	}

	recordAsJon, err := json.Marshal(resp)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(recordAsJon)
	return

}

func (web *WebServicer) Calculate(w http.ResponseWriter, r *http.Request) {

	requestAmountAsStr := r.URL.Query().Get("requestAmount")
	currentAmountStr := r.URL.Query().Get("currentAmount")
	everyTxAmountAsStr := r.URL.Query().Get("everyTxAmount")
	txNumStr := r.URL.Query().Get("txNum")

	requestAmount, err := decimal.NewFromString(requestAmountAsStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	currentAmount, err := decimal.NewFromString(currentAmountStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	everyTxAmount, err := decimal.NewFromString(everyTxAmountAsStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	txNum, err := decimal.NewFromString(txNumStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	res := requestAmount.Add(currentAmount).Sub(everyTxAmount.Mul(txNum))

	resAsJson, err := json.Marshal(res)

	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(resAsJson)

}

func (web *WebServicer) WebService(config Config) error {
	r := mux.NewRouter()
	// Routes consist of a path and a handler function.
	r.HandleFunc("/sender/param", web.ParamHandler)
	//r.HandleFunc("/faucet/{addr}", web.FaucetHandler)
	r.HandleFunc("/sender/process/{uuid}", web.ProcessHandler)
	r.HandleFunc("/sender/tree/{uuid}", web.GetTreeHandler)
	r.HandleFunc("/sender/txslist/{hash}", web.GetTxsList)
	r.HandleFunc("/sender/txs", web.TxsHandler).Methods("POST")
	r.HandleFunc("/sender/calculate", web.Calculate)

	//r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(config.WebStaticDir))))

	clientFactory, exists := clientFactories["ethclient"]
	if !exists {
		return fmt.Errorf("unrecognized client factory: %s", "ethclient")
	}
	client, err := clientFactory.NewClient(config)

	if err != nil {
		return err
	}

	web.cli = client

	transactor, err := NewTransactor(config.WebsocketAddr, &config)

	if err != nil {
		return err
	}

	transactor.Start()
	web.transactor = transactor

	handler := cors.Default().Handler(r)
	// Bind to a port and pass our router in
	return http.ListenAndServe(":8000", handler)
}

// func (web *WebServicer) FaucetHandler(w http.ResponseWriter, r *http.Request) {

// 	vars := mux.Vars(r)
// 	myString := vars["addr"]

// 	fmt.Printf("request addr: %s \n", myString)

// 	w.Header().Set("Content-Type", "application/json")
// 	w.WriteHeader(http.StatusOK)

// 	w.Write([]byte(myString))

// }

func (web *WebServicer) ProcessHandler(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)
	uuidStr := vars["uuid"]

	id, err := uuid.Parse(uuidStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if record, ok := GetSendRecord(id); ok {

		sent := 0

		for _, v := range record.SendRecord {
			sent += v.TxNum
		}

		quantity := decimal.NewFromInt(int64(sent)).Div(decimal.NewFromInt(record.TotalConsTx))

		_ = quantity
		record.Percent = FloatRound(float64(sent)/float64(record.TotalConsTx), 2)
		recordAsJon, err := json.Marshal(record)

		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write(recordAsJon)
		return
	}

	if err != nil {
		http.Error(w, fmt.Sprintf("can not find the uuid: %s ", id.String()), http.StatusInternalServerError)
		return
	}

}

func FloatRound(f float64, n int) float64 {
	format := "%." + strconv.Itoa(n) + "f"
	res, _ := strconv.ParseFloat(fmt.Sprintf(format, f), 64)
	return res
}

type TxParams struct {
	From            string   `json:"from,omitempty"`
	RequestAmount   int64    `json:"request_amount"`
	ToAddrs         []string `json:"to_addrs"`
	BatchTxSize     int64    `json:"batch_tx_size,omitempty"`
	BatchTxHashSize int64    `json:"batch_tx_hash_size,omitempty"`
	TxCount         int64    `json:"tx_count"`
	EveryTxAmount   int64    `json:"everyTxAmount"`
	//Rate            int64    `json:"rate"`
	//ExpectedTime    int64    `json:"expected_time"`
}

func (web *WebServicer) TxsHandler(w http.ResponseWriter, r *http.Request) {

	var params TxParams
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&params); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if params.TxCount > MaxTxCount {
		http.Error(w, fmt.Sprintf("max tx count %d ", MaxTxCount), http.StatusBadRequest)
		return
	}

	if atomic.LoadInt32(&Running) != 0 {
		http.Error(w, fmt.Sprintf("another batch txs is running "), http.StatusBadRequest)
		return
	}

	atomic.StoreInt32(&Running, 1)

	if params.RequestAmount > MaxRequestAmount || params.RequestAmount < params.EveryTxAmount*params.TxCount {

		http.Error(w, fmt.Sprintf("params.RequestAmount %d > MaxRequestAmount %d|| params.RequestAmount %d < params.EveryTxAmount*params.TxCount %d *%d ", params.RequestAmount, MaxRequestAmount, params.RequestAmount, params.EveryTxAmount, params.TxCount), http.StatusBadRequest)
		return

	}

	if len(params.ToAddrs) == 0 {

		http.Error(w, fmt.Sprintf("expected params.ToAddrs length >0 but actually: %d ", len(params.ToAddrs)), http.StatusBadRequest)
		return

	}

	id := uuid.New()

	//consTxNum := 0

	total := int(params.TxCount)

	if params.From != systemFromAddr {
		total = total + 1
	}

	// consTxNum = total / (batchTxHashSize * batchTxSize)
	// if total%(batchTxHashSize*batchTxSize) != 0 {
	// 	consTxNum = consTxNum + 1
	// }

	SetSendRecord(id, SendRecord{TotalConsTx: int64(total)})

	go func() {

		if err := web.sendLoop(params.From, params.ToAddrs, int(params.TxCount), params.EveryTxAmount, id, params.RequestAmount); err != nil {
			//if err := web.ProduceTxs(params.From, params.ToAddrs, int(params.TxCount), params.EveryTxAmount, id, params.RequestAmount); err != nil {
			fmt.Printf("web send loop, id: %s   err: %s \n", id, err.Error())
		}

		atomic.StoreInt32(&Running, 0)
	}()

	resAsJson, err := json.Marshal(id)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	fmt.Printf("len(resAsJson): %d \n", len(resAsJson))

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(resAsJson)

}

type BatchTx struct {
	BatchHash      string   `json:"batch_hash"`
	OriginalTxHash []string `json:"original_tx_hash"`
}

type ConsTxHashs struct {
	TxHash  []byte    `json:"tx_hash"`
	Batches []BatchTx `json:"batches"`
}

type ConsTxHash struct {
	ConsTxHash   []byte
	BatchTxsHash [][]byte
}

type WebResp struct {
	ProcessId uuid.UUID     `json:"process_id"`
	AllTxs    []ConsTxHashs `json:"all_txs"`
}

func (web *WebServicer) sendLoop(fromAddr string, toAddrs []string, txCount int, amount int64, id uuid.UUID, requestAmount int64) error {

	addrsL := len(toAddrs)
	first := true

	sendTicker := time.NewTicker(time.Duration(3) * time.Second)

	for {
		select {
		case <-sendTicker.C:
			var hashesBytes []byte = make([]byte, 0, 32*batchTxHashSize)
			var beginOriginalTx common.Hash
			var endOriginalTx common.Hash
			var sendRedisBeginTime time.Time

			var beginTotal = txCount

			for j := 0; j < batchTxHashSize; j++ {
				var txsBytes []byte
				var txs []TxWithFrom = make([]TxWithFrom, 0, batchTxSize)

				txshash := make([]string, 0, batchTxSize)
				for i := 0; i < batchTxSize; i++ {

					var tx *types.Transaction
					var err error

					//fmt.Printf("param fromAddr: %s systemFromAddr: %s result: %v \n ", fromAddr, systemFromAddr, (fromAddr != systemFromAddr && first))

					if fromAddr != systemFromAddr && first {
						txCount++
						beginTotal++
						tx, err = buildOriginalTx(originalTxParam.Nonce, common.HexToAddress(fromAddr), requestAmount, big.NewInt(256), nil)

					} else {

						//fmt.Printf("amount: %d  idx: %d  addrsL: %d \n", amount, i, addrsL)
						tx, err = buildOriginalTx(originalTxParam.Nonce, common.HexToAddress(toAddrs[i%addrsL]), amount, big.NewInt(256), nil)
					}

					if err != nil {
						return err
					}

					if j == i && i == 0 {
						beginOriginalTx = tx.Hash()
					}

					endOriginalTx = tx.Hash()

					originalTxParam.Nonce += 1

					txAsBytes, err := tx.MarshalBinary()
					if err != nil {
						return err
					}

					txsBytes = append(txsBytes, txAsBytes...)

					if fromAddr != systemFromAddr && first {
						txs = append(txs, TxWithFrom{
							common.HexToAddress(systemFromAddr).Bytes(),
							tx})
						first = false
					} else {
						txs = append(txs, TxWithFrom{
							common.HexToAddress(fromAddr).Bytes(),
							tx})
					}

					txshash = append(txshash, tx.Hash().String())

					txCount--

					if txCount == 0 {
						break
					}
				}

				h := sha256.New()
				if _, err := h.Write(txsBytes); err != nil {
					return err
				}

				hashBytes := h.Sum(nil)
				hashesBytes = append(hashesBytes, hashBytes...)

				batchTxs := OriginalBatchTxs{Hash: hashBytes, Txs: txs}

				batchTxsForRedis <- &batchTxs
				if j == 0 {
					sendRedisBeginTime = time.Now()
				}

				if txCount == 0 {
					break
				}
			}

			tx, err := web.cli.BuildTx(&hashesBytes)

			if err != nil {
				return err
			}

			if err := web.transactor.SendTx(tx); err != nil {
				return err
			}

			consTxWithBatchs := []ConsTxWithBatchHash{}

			consTxWithBatchs = append(consTxWithBatchs, ConsTxWithBatchHash{ConsTxHash: tx.Hash().Bytes(),
				BatchTxsHash: hashesBytes})

			if record, ok := GetSendRecord(id); ok {
				b := BatchSend{
					BeginOriginalTx:      beginOriginalTx,
					EndOriginalTx:        endOriginalTx,
					SendToRedisBeginTime: sendRedisBeginTime.Unix(),
					SendTxsEndTime:       time.Now().Unix(),
					TxNum:                beginTotal - txCount,
					ConsTxWithBatchHash:  consTxWithBatchs,
				}

				record.SendRecord = append(record.SendRecord, b)
				SetSendRecord(id, record)

			}

			if txCount == 0 {
				break
			}
		}

		if txCount == 0 {
			break
		}
	}

	return nil
}

func (web *WebServicer) ProduceTxs(fromAddr string, toAddrs []string, txCount int, amount int64, id uuid.UUID, requestAmount int64) error {

	addrsL := len(toAddrs)
	first := true

	for {

		var hashesBytes []byte = make([]byte, 0, 32*batchTxHashSize)
		var beginOriginalTx common.Hash
		var endOriginalTx common.Hash
		var sendRedisBeginTime time.Time

		for j := 0; j < batchTxHashSize; j++ {
			var txsBytes []byte
			var txs []TxWithFrom = make([]TxWithFrom, 0, batchTxSize)

			txshash := make([]string, 0, batchTxSize)
			for i := 0; i < batchTxSize; i++ {

				var tx *types.Transaction
				var err error

				//fmt.Printf("param fromAddr: %s systemFromAddr: %s result: %v \n ", fromAddr, systemFromAddr, (fromAddr != systemFromAddr && first))

				if fromAddr != systemFromAddr && first {
					txCount++
					tx, err = buildOriginalTx(originalTxParam.Nonce, common.HexToAddress(fromAddr), requestAmount, big.NewInt(256), nil)

				} else {

					//fmt.Printf("amount: %d  idx: %d  addrsL: %d \n", amount, i, addrsL)
					tx, err = buildOriginalTx(originalTxParam.Nonce, common.HexToAddress(toAddrs[i%addrsL]), amount, big.NewInt(256), nil)
				}

				if err != nil {
					return err
				}

				if j == i && i == 0 {
					beginOriginalTx = tx.Hash()
				}

				endOriginalTx = tx.Hash()

				originalTxParam.Nonce += 1

				txAsBytes, err := tx.MarshalBinary()
				if err != nil {
					return err
				}

				txsBytes = append(txsBytes, txAsBytes...)

				if fromAddr != systemFromAddr && first {
					txs = append(txs, TxWithFrom{
						common.HexToAddress(systemFromAddr).Bytes(),
						tx})
					first = false
				} else {
					txs = append(txs, TxWithFrom{
						common.HexToAddress(fromAddr).Bytes(),
						tx})
				}

				txshash = append(txshash, tx.Hash().String())

				txCount--

				if txCount == 0 {
					break
				}
			}

			h := sha256.New()
			if _, err := h.Write(txsBytes); err != nil {
				return err
			}

			hashBytes := h.Sum(nil)
			hashesBytes = append(hashesBytes, hashBytes...)

			batchTxs := OriginalBatchTxs{Hash: hashBytes, Txs: txs}

			batchTxsForRedis <- &batchTxs
			if j == 0 {
				sendRedisBeginTime = time.Now()
			}

			if txCount == 0 {
				break
			}
		}

		originalTxsHashQueue <- &hashesBytes //和下一行改为同步模式
		tx, err := web.cli.GenerateTx()
		if err != nil {
			return err
		}

		conTxsQueue <- ConTxsWithId{
			SendRedisTime:   sendRedisBeginTime,
			BeginOriginalTx: beginOriginalTx,
			EndOriginalTx:   endOriginalTx,
			Id:              id,
			Tx:              tx,
			BatchTxHash:     hashesBytes}

		if txCount == 0 {
			break
		}
	}

	return nil
}
