// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package rpctest

import (
	"context"
	"fmt"
	"maps"
	"math/rand"
	"slices"
	"sort"
	"sync"
	"time"

	"golang.org/x/sync/errgroup"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/estimate"
	"github.com/erigontech/erigon/common/hexutil"
	"github.com/erigontech/erigon/common/log/v3"
)

// BenchEthGetLogs compares response of Erigon with Geth
// but also can be used for comparing RPCDaemon with Geth or infura
// parameters:
// needCompare - if false - doesn't call Erigon and doesn't compare responses
//
//	false value - to generate vegeta files, it's faster but we can generate vegeta files for Geth and Erigon
//	recordFile stores all eth_getlogs returned with success
//	errorFile stores information when erigon and geth doesn't return same data
func BenchEthGetLogs(erigonURL, gethURL string, needCompare bool, blockFrom uint64, blockTo uint64, recordFile string, errorFile string) error {
	setRoutes(erigonURL, gethURL)

	rec, errs, cleanup, err := openWriters(recordFile, errorFile)
	if err != nil {
		return err
	}
	defer cleanup()

	var resultsCh chan CallResult = nil
	if !needCompare {
		resultsCh = make(chan CallResult, 1000)
		defer close(resultsCh)
		go vegetaWrite(true, []string{"debug_getModifiedAccountsByNumber", "eth_getLogs"}, resultsCh)
	}

	var res CallResult
	reqGen := &RequestGenerator{}

	var blockNumber EthBlockNumber
	res = reqGen.Erigon("eth_blockNumber", reqGen.blockNumber(), &blockNumber)
	if res.Err != nil {
		return fmt.Errorf("Could not get block number: %v\n", res.Err)
	}
	if blockNumber.Error != nil {
		return fmt.Errorf("Error getting block number: %d %s\n", blockNumber.Error.Code, blockNumber.Error.Message)
	}
	fmt.Printf("Last block: %d\n", blockNumber.Number)

	prevBn := blockFrom
	rnd := rand.New(rand.NewSource(42)) // nolint:gosec
	for bn := blockFrom + 100; bn < blockTo; bn += 100 {

		// Checking modified accounts

		var mag DebugModifiedAccounts
		res = reqGen.Erigon("debug_getModifiedAccountsByNumber", reqGen.getModifiedAccountsByNumber(prevBn, bn), &mag)
		if res.Err != nil {
			return fmt.Errorf("Could not get modified accounts (Erigon): %v\n", res.Err)
		}
		if mag.Error != nil {
			return fmt.Errorf("Error getting modified accounts (Erigon): %d %s\n", mag.Error.Code, mag.Error.Message)
		}
		if res.Err == nil && mag.Error == nil {
			accountSet := extractAccountMap(&mag)
			for account := range accountSet {

				request := reqGen.getLogs(prevBn, bn, account)
				errCtx := fmt.Sprintf("account %x blocks %d-%d", account, prevBn, bn)
				if err := requestAndCompare(request, "eth_getLogs", errCtx, reqGen, needCompare, rec, errs, resultsCh,
					/* insertOnlyIfSuccess */ false); err != nil {
					fmt.Println(err)
					return err
				}
				topics := getTopics(res.Result)
				// All combination of account and one topic
				for _, topic := range topics {

					request = reqGen.getLogs1(prevBn, bn+10000, account, topic)
					errCtx := fmt.Sprintf("account %x topic %x blocks %d-%d", account, topic, prevBn, bn)
					if err := requestAndCompare(request, "eth_getLogs", errCtx, reqGen, needCompare, rec, errs, resultsCh,
						/* insertOnlyIfSuccess */ false); err != nil {
						fmt.Println(err)
						return err
					}
				}
				// Random combinations of two topics
				if len(topics) >= 2 {
					idx1 := rnd.Int31n(int32(len(topics)))
					idx2 := rnd.Int31n(int32(len(topics) - 1))
					if idx2 >= idx1 {
						idx2++
					}

					request = reqGen.getLogs2(prevBn, bn+100000, account, topics[idx1], topics[idx2])
					errCtx := fmt.Sprintf("account %x topic1 %x topic2 %x blocks %d-%d", account, topics[idx1], topics[idx2], prevBn, bn)
					if err := requestAndCompare(request, "eth_getLogs", errCtx, reqGen, needCompare, rec, errs, resultsCh,
						/* insertOnlyIfSuccess */ false); err != nil {
						fmt.Println(err)
						return err
					}
				}
			}
		}
		fmt.Printf("Done blocks %d-%d, modified accounts: %d\n", prevBn, bn, len(mag.Result))
		prevBn = bn
	}
	return nil
}

func EthGetLogsInvariants(ctx context.Context, erigonURL, gethURL string, needCompare bool, blockFrom, blockTo uint64, latest, failFast bool) error {
	setRoutes(erigonURL, gethURL)

	reqGen := &RequestGenerator{}

	var blockNumber EthBlockNumber
	res := reqGen.Erigon("eth_blockNumber", reqGen.blockNumber(), &blockNumber)
	if res.Err != nil {
		return fmt.Errorf("could not get block number: %v", res.Err)
	}
	if blockNumber.Error != nil {
		return fmt.Errorf("error getting block number: %d %s", blockNumber.Error.Code, blockNumber.Error.Message)
	}
	latestBlock := blockNumber.Number.Uint64()
	log.Info("[ethGetLogsInvariants] starting", "blockFrom", blockFrom, "blockTo", blockTo, "latestBlock", latestBlock)
	if blockFrom > latestBlock {
		return fmt.Errorf("fromBlock(%d) > latestBlock(%d)", blockFrom, latestBlock)
	}
	if blockTo > latestBlock {
		log.Info("[ethGetLogsInvariants] blockTo > latestBlock, setting blockTo to latestBlock", "blockTo", blockTo, "latestBlock", latestBlock)
	}

	logEvery, lastLoggedBlock, lastLoggedTime := time.NewTicker(20*time.Second), blockFrom, time.Now()
	defer logEvery.Stop()

	noDuplicates := func(logs []Log) error {
		if len(logs) <= 1 {
			return nil
		}
		seen := make(map[hexutil.Uint]struct{}, len(logs))
		for _, l := range logs {
			if _, ok := seen[l.Index]; ok {
				return fmt.Errorf("duplicated log_index %d", l.Index)
			}
			seen[l.Index] = struct{}{}
		}
		return nil
	}

	eg := &errgroup.Group{}
	eg.SetLimit(estimate.AlmostAllCPUs())

	for bn := blockFrom; bn < blockTo; bn++ {
		bn := bn
		eg.Go(func() error {
			var resp EthGetLogs
			res := reqGen.Erigon("eth_getLogs", reqGen.getLogsNoFilters(bn, bn), &resp)
			baseOK := true
			if res.Err != nil {
				if failFast {
					return fmt.Errorf("could not get eth_getLogs baseline (Erigon): %v", res.Err)
				}
				log.Error("[ethGetLogsInvariants] could not get eth_getLogs baseline", "blockNum", bn, "error", res.Err.Error())
				baseOK = false
			}
			if resp.Error != nil {
				if failFast {
					return fmt.Errorf("error getting eth_getLogs baseline (Erigon): %d %s", resp.Error.Code, resp.Error.Message)
				}
				log.Error("[ethGetLogsInvariants] error getting eth_getLogs baseline", "blockNum", bn, "error", resp.Error.Code, "message", resp.Error.Message)
				baseOK = false
			}
			if baseOK {
				if err := noDuplicates(resp.Result); err != nil {
					if failFast {
						return fmt.Errorf("eth_getLogs: at blockNum=%d %w", bn, err)
					} else {
						log.Error("[ethGetLogsInvariants] eth_getLogs: noDuplicates", "blockNum", bn, "error", err.Error())
					}
				}
			}

			sawAddr := map[common.Address]struct{}{}
			topicsByPos := [4]map[common.Hash]struct{}{{}, {}, {}, {}}
			if baseOK {
				for _, l := range resp.Result {
					sawAddr[l.Address] = struct{}{}
					for pos, t := range l.Topics {
						if pos < 4 {
							topicsByPos[pos][t] = struct{}{}
						}
					}
				}
			}

			if len(sawAddr) > 0 {
				resp = EthGetLogs{}
				res = reqGen.Erigon("eth_getLogs", reqGen.getLogsForAddresses(bn, bn, slices.Collect(maps.Keys(sawAddr))), &resp)
				addrOK := true
				if res.Err != nil {
					if failFast {
						return fmt.Errorf("could not get eth_getLogs by address (Erigon): %v", res.Err)
					}
					log.Error("[ethGetLogsInvariants] could not get eth_getLogs by address", "blockNum", bn, "error", res.Err.Error())
					addrOK = false
				}
				if resp.Error != nil {
					if failFast {
						return fmt.Errorf("error getting eth_getLogs by address (Erigon): %d %s", resp.Error.Code, resp.Error.Message)
					}
					log.Error("[ethGetLogsInvariants] error getting eth_getLogs by address", "blockNum", bn, "error", resp.Error.Code, "message", resp.Error.Message)
					addrOK = false
				}

				if addrOK {
					for k := range sawAddr {
						logs := filterLogsByAddr(resp.Result, k)
						if len(logs) == 0 {
							if failFast {
								return fmt.Errorf("eth_getLogs: at blockNum=%d and addr %x not indexed", bn, k)
							} else {
								log.Error("[ethGetLogsInvariants] eth_getLogs not indexed", "blockNum", bn, "addr", k.Hex())
							}
						}
						if err := noDuplicates(logs); err != nil {
							if failFast {
								return fmt.Errorf("eth_getLogs: at blockNum=%d and addr %x %w", bn, k, err)
							} else {
								log.Error("[ethGetLogsInvariants] eth_getLogs: noDuplicates", "blockNum", bn, "addr", k.Hex(), "error", err.Error())
							}
						}
					}
				}
			}

			for pos, topicsAtPos := range topicsByPos {
				if len(topicsAtPos) == 0 {
					continue
				}
				filter := make([][]common.Hash, pos+1)
				filter[pos] = slices.Collect(maps.Keys(topicsAtPos))

				resp = EthGetLogs{}
				res = reqGen.Erigon("eth_getLogs", reqGen.getLogsForTopics(bn, bn, filter), &resp)
				if res.Err != nil {
					if failFast {
						return fmt.Errorf("could not get logs by topics pos %d (Erigon): %v", pos, res.Err)
					}
					log.Error("[ethGetLogsInvariants] could not get logs by topics (Erigon)", "blockNum", bn, "pos", pos, "error", res.Err.Error())
					continue
				}
				if resp.Error != nil {
					if failFast {
						return fmt.Errorf("error getting logs by topics pos %d (Erigon): %d %s", pos, resp.Error.Code, resp.Error.Message)
					}
					log.Error("[ethGetLogsInvariants] error getting logs by topics (Erigon)", "blockNum", bn, "pos", pos, "error", resp.Error.Code, "message", resp.Error.Message)
					continue
				}

				logsByTopic := make(map[common.Hash][]Log, len(topicsAtPos))
				for _, l := range resp.Result {
					if pos < len(l.Topics) {
						t := l.Topics[pos]
						if _, ok := topicsAtPos[t]; ok {
							logsByTopic[t] = append(logsByTopic[t], l)
						}
					}
				}

				for k := range topicsAtPos {
					logs := logsByTopic[k]
					if len(logs) == 0 {
						if failFast {
							return fmt.Errorf("eth_getLogs: at blockNum=%d topic %x pos %d not indexed", bn, k, pos)
						} else {
							log.Error("[ethGetLogsInvariants] eth_getLogs topic not indexed", "blockNum", bn, "topic", k.Hex(), "pos", pos)
						}
					}
					if err := noDuplicates(logs); err != nil {
						if failFast {
							return fmt.Errorf("eth_getLogs: at blockNum=%d topic %x pos %d %w", bn, k, pos, err)
						} else {
							log.Error("[ethGetLogsInvariants] eth_getLogs: noDuplicates", "blockNum", bn, "topic", k.Hex(), "pos", pos, "error", err.Error())
						}
					}
				}
			}

			select {
			case <-ctx.Done():
				return ctx.Err()
			default:
			}
			return nil
		})

		if bn%1000 == 0 || bn == blockTo-1 {
			if err := eg.Wait(); err != nil {
				return err
			}
		}

		select {
		case <-logEvery.C:
			blocksProcessed := bn - lastLoggedBlock
			blocksPerSec := int(float64(blocksProcessed) / time.Since(lastLoggedTime).Seconds())

			lastLoggedBlock = bn
			lastLoggedTime = time.Now()

			log.Info("[ethGetLogsInvariants]", "block_num", fmt.Sprintf("%.2fm", float64(bn)/1_000_000), "blk/s", blocksPerSec)
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
	}
	return nil
}

func BenchEthGetLogsRandomBlock(erigonURL string, concurentRequests int) error {
	setRoutes(erigonURL, "")

	reqGen := &RequestGenerator{}

	var res CallResult

	var blockNumber EthBlockNumber
	res = reqGen.Erigon("eth_blockNumber", reqGen.blockNumber(), &blockNumber)
	if res.Err != nil {
		return fmt.Errorf("Could not get block number: %v\n", res.Err)
	}
	if blockNumber.Error != nil {
		return fmt.Errorf("Error getting block number: %d %s\n", blockNumber.Error.Code, blockNumber.Error.Message)
	}

	latencyLen := 1000
	latency := make([]int, 0, latencyLen)
	var m sync.Mutex
	var finishedRequests uint64

	go func() {
		lastPrint := time.Now()
		p50 := float64(0)
		p90 := float64(0)
		p99 := float64(0)
		rps := float64(0)

		for {

			time.Sleep(time.Second)
			m.Lock()

			toPrint := make([]int, len(latency))
			copy(toPrint, latency)

			if len(toPrint) > 0 {
				sort.Ints(toPrint)

				p50 = float64(toPrint[len(toPrint)/2]) / 1000 // convert to ms
				p90 = float64(toPrint[len(toPrint)/10*9]) / 1000
				p99 = float64(toPrint[len(toPrint)/100*99]) / 1000
				rps = float64(finishedRequests) / float64(time.Since(lastPrint).Seconds())

				lastPrint = time.Now()
				finishedRequests = 0
			}

			m.Unlock()

			fmt.Printf("Latency 50p: %.2fms 90p: %.2fms 99p: %.2fms RPS: %.2f req/s\n",
				p50,
				p90,
				p99,
				rps,
			)

		}

	}()

	reqQueue := make(chan struct{}, concurentRequests)

	for {
		bn := uint64(rand.Intn(
			int(blockNumber.Number.Uint64()),
		))

		reqQueue <- struct{}{}

		go func(bn uint64, launchedAt time.Time) {
			var resp EthGetLogs
			res := reqGen.Erigon("eth_getLogs", reqGen.getLogsNoFilters(bn, bn), &resp)
			if res.Err != nil {
				panic(fmt.Errorf("[ethGetLogsRandomBlock] could not get logs bn %d err %s", bn, res.Err.Error()))
			}
			if resp.Error != nil {
				panic(fmt.Errorf("Error getting logs (Erigon): %d %s", resp.Error.Code, resp.Error.Message))
			}

			reqLatency := int(time.Since(launchedAt).Microseconds())
			<-reqQueue

			m.Lock()
			finishedRequests += 1

			if len(latency) >= latencyLen {
				latency = latency[1:]
			}

			latency = append(latency, reqLatency)
			m.Unlock()
		}(bn, time.Now())
	}
}

func filterLogsByAddr(logs []Log, addr common.Address) (filtered []Log) {
	for _, log := range logs {
		if log.Address == addr {
			filtered = append(filtered, log)
		}
	}
	return
}
