// 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 shards

import (
	"sync"
	"sync/atomic"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/node/gointerfaces"
	"github.com/erigontech/erigon/node/gointerfaces/remoteproto"
	"github.com/erigontech/erigon/node/gointerfaces/typesproto"
)

type NewSnapshotSubscription func() error
type HeaderSubscription func(headerRLP []byte) error
type PendingLogsSubscription func(types.Logs) error
type PendingBlockSubscription func(*types.Block) error
type PendingTxsSubscription func([]types.Transaction) error
type LogsSubscription func([]*remoteproto.SubscribeLogsReply) error

// Events manages event subscriptions and dissimination. Thread-safe
type Events struct {
	id                          int
	headerSubscriptions         map[int]chan [][]byte
	newSnapshotSubscription     map[int]chan struct{}
	retirementStartSubscription map[int]chan bool
	retirementDoneSubscription  map[int]chan struct{}
	pendingLogsSubscriptions    map[int]PendingLogsSubscription
	pendingBlockSubscriptions   map[int]PendingBlockSubscription
	pendingTxsSubscriptions     map[int]PendingTxsSubscription
	logsSubscriptions           map[int]chan []*remoteproto.SubscribeLogsReply
	hasLogSubscriptions         bool
	receiptsSubscriptions       map[int]chan []*remoteproto.SubscribeReceiptsReply
	hasReceiptSubscriptions     bool
	lock                        sync.RWMutex
}

func NewEvents() *Events {
	return &Events{
		headerSubscriptions:         map[int]chan [][]byte{},
		receiptsSubscriptions:       map[int]chan []*remoteproto.SubscribeReceiptsReply{},
		pendingLogsSubscriptions:    map[int]PendingLogsSubscription{},
		pendingBlockSubscriptions:   map[int]PendingBlockSubscription{},
		pendingTxsSubscriptions:     map[int]PendingTxsSubscription{},
		logsSubscriptions:           map[int]chan []*remoteproto.SubscribeLogsReply{},
		newSnapshotSubscription:     map[int]chan struct{}{},
		retirementStartSubscription: map[int]chan bool{},
		retirementDoneSubscription:  map[int]chan struct{}{},
	}
}

func (e *Events) AddHeaderSubscription() (chan [][]byte, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan [][]byte, 8)
	e.id++
	id := e.id
	e.headerSubscriptions[id] = ch
	return ch, func() {
		e.lock.Lock()
		defer e.lock.Unlock()
		delete(e.headerSubscriptions, id)
		close(ch)
	}
}

func (e *Events) AddReceiptsSubscription() (chan []*remoteproto.SubscribeReceiptsReply, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan []*remoteproto.SubscribeReceiptsReply, 8)
	e.id++
	id := e.id
	e.receiptsSubscriptions[id] = ch
	return ch, func() {
		delete(e.receiptsSubscriptions, id)
		close(ch)
	}
}

func (e *Events) EmptyReceiptSubscription(empty bool) {
	e.lock.Lock()
	defer e.lock.Unlock()
	e.hasReceiptSubscriptions = !empty
}

func (e *Events) HasReceiptSubscriptions() bool {
	e.lock.RLock()
	defer e.lock.RUnlock()
	return e.hasReceiptSubscriptions
}

func (e *Events) AddNewSnapshotSubscription() (chan struct{}, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan struct{}, 8)
	e.id++
	id := e.id
	e.newSnapshotSubscription[id] = ch
	return ch, func() {
		delete(e.newSnapshotSubscription, id)
		close(ch)
	}
}

func (e *Events) AddRetirementStartSubscription() (chan bool, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan bool, 8)
	e.id++
	id := e.id
	e.retirementStartSubscription[id] = ch
	return ch, func() {
		delete(e.retirementStartSubscription, id)
		close(ch)
	}
}

func (e *Events) AddRetirementDoneSubscription() (chan struct{}, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan struct{}, 8)
	e.id++
	id := e.id
	e.retirementDoneSubscription[id] = ch
	return ch, func() {
		delete(e.retirementDoneSubscription, id)
		close(ch)
	}
}

func (e *Events) AddLogsSubscription() (chan []*remoteproto.SubscribeLogsReply, func()) {
	e.lock.Lock()
	defer e.lock.Unlock()
	ch := make(chan []*remoteproto.SubscribeLogsReply, 8)
	e.id++
	id := e.id
	e.logsSubscriptions[id] = ch
	return ch, func() {
		delete(e.logsSubscriptions, id)
		close(ch)
	}
}

func (e *Events) EmptyLogSubscription(empty bool) {
	e.lock.Lock()
	defer e.lock.Unlock()
	e.hasLogSubscriptions = !empty
}

func (e *Events) HasLogSubscriptions() bool {
	e.lock.RLock()
	defer e.lock.RUnlock()
	return e.hasLogSubscriptions
}

func (e *Events) AddPendingLogsSubscription(s PendingLogsSubscription) {
	e.lock.Lock()
	defer e.lock.Unlock()
	e.pendingLogsSubscriptions[len(e.pendingLogsSubscriptions)] = s
}

func (e *Events) AddPendingBlockSubscription(s PendingBlockSubscription) {
	e.lock.Lock()
	defer e.lock.Unlock()
	e.pendingBlockSubscriptions[len(e.pendingBlockSubscriptions)] = s
}

func (e *Events) OnNewSnapshot() {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.newSnapshotSubscription {
		common.PrioritizedSend(ch, struct{}{})
	}
}

func (e *Events) OnNewHeader(newHeadersRlp [][]byte) {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.headerSubscriptions {
		common.PrioritizedSend(ch, newHeadersRlp)
	}
}

func (e *Events) OnNewPendingLogs(logs types.Logs) {
	e.lock.Lock()
	defer e.lock.Unlock()
	for i, sub := range e.pendingLogsSubscriptions {
		if err := sub(logs); err != nil {
			delete(e.pendingLogsSubscriptions, i)
		}
	}
}

func (e *Events) OnLogs(logs []*remoteproto.SubscribeLogsReply) {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.logsSubscriptions {
		common.PrioritizedSend(ch, logs)
	}
}

func (e *Events) OnReceipts(receipts []*remoteproto.SubscribeReceiptsReply) {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.receiptsSubscriptions {
		common.PrioritizedSend(ch, receipts)
	}
}

func (e *Events) OnRetirementStart(started bool) {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.retirementStartSubscription {
		common.PrioritizedSend(ch, started)
	}
}

func (e *Events) OnRetirementDone() {
	e.lock.Lock()
	defer e.lock.Unlock()
	for _, ch := range e.retirementDoneSubscription {
		common.PrioritizedSend(ch, struct{}{})
	}
}

type Notifications struct {
	Events               *Events
	Accumulator          *Accumulator // StateAccumulator
	StateChangesConsumer StateChangeConsumer
	RecentReceipts       *RecentReceipts
	LastNewBlockSeen     atomic.Uint64 // This is used by eth_syncing as an heuristic to determine if the node is syncing or not.
}

func (n *Notifications) NewLastBlockSeen(blockNum uint64) {
	n.LastNewBlockSeen.Store(blockNum)
}

func NewNotifications(StateChangesConsumer StateChangeConsumer) *Notifications {
	return &Notifications{
		Events:               NewEvents(),
		Accumulator:          NewAccumulator(),
		RecentReceipts:       NewRecentReceipts(512),
		StateChangesConsumer: StateChangesConsumer,
	}
}

// Requirements:
// - Erigon3 doesn't store logs in db (yet)
// - need support unwind of receipts
// - need send notification after `rwtx.Commit` (or user will recv notification, but can't request new data by RPC)
type RecentReceipts struct {
	receipts map[uint64]types.Receipts
	txs      map[uint64][]types.Transaction
	headers  map[uint64]*types.Header
	limit    uint64
	mu       sync.Mutex
}

func NewRecentReceipts(limit uint64) *RecentReceipts {
	return &RecentReceipts{
		receipts: make(map[uint64]types.Receipts, limit),
		txs:      make(map[uint64][]types.Transaction, limit),
		headers:  make(map[uint64]*types.Header, limit),
		limit:    limit,
	}
}

// Notify sends log notifications (for logs subscription)
// [from,to)
func (r *RecentReceipts) NotifyLogs(n *Events, from, to uint64, isUnwind bool) {
	if !n.HasLogSubscriptions() {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	for bn, receipts := range r.receipts {
		if bn+r.limit < from { // evict old
			delete(r.receipts, bn)
			continue
		}
		if bn < from || bn >= to {
			continue
		}

		var blockNum uint64
		reply := make([]*remoteproto.SubscribeLogsReply, 0, len(receipts))
		for _, receipt := range receipts {
			if receipt == nil {
				continue
			}

			blockNum = receipt.BlockNumber.Uint64()
			//txIndex++
			//// bor transactions are at the end of the bodies transactions (added manually but not actually part of the block)
			//if txIndex == uint64(len(block.Transactions())) {
			//	txHash = bortypes.ComputeBorTxHash(blockNum, block.Hash())
			//} else {
			//	txHash = block.Transactions()[txIndex].Hash()
			//}

			for _, l := range receipt.Logs {
				res := &remoteproto.SubscribeLogsReply{
					Address:          gointerfaces.ConvertAddressToH160(l.Address),
					BlockHash:        gointerfaces.ConvertHashToH256(receipt.BlockHash),
					BlockNumber:      blockNum,
					Data:             l.Data,
					LogIndex:         uint64(l.Index),
					Topics:           make([]*typesproto.H256, 0, len(l.Topics)),
					TransactionHash:  gointerfaces.ConvertHashToH256(receipt.TxHash),
					TransactionIndex: uint64(l.TxIndex),
					Removed:          isUnwind,
				}
				for _, topic := range l.Topics {
					res.Topics = append(res.Topics, gointerfaces.ConvertHashToH256(topic))
				}
				reply = append(reply, res)
			}
		}

		n.OnLogs(reply)
	}
}

func (r *RecentReceipts) Add(receipts types.Receipts, txs []types.Transaction, header *types.Header) {
	if len(receipts) == 0 {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	var blockNum uint64
	var ok bool
	// find non-nil receipt
	for _, receipt := range receipts {
		if receipt != nil {
			ok = true
			blockNum = receipt.BlockNumber.Uint64()
			break
		}
	}
	if !ok {
		return
	}
	r.receipts[blockNum] = receipts
	r.txs[blockNum] = txs
	r.headers[blockNum] = header

	// enforce `limit`: drop all items older than `limit` blocks
	if len(r.receipts) <= int(r.limit) {
		return
	}
	for bn := range r.receipts {
		if bn+r.limit < blockNum {
			delete(r.receipts, bn)
			delete(r.txs, bn)
			delete(r.headers, bn)
		}
	}
}

// NotifyReceipts sends receipt Proto notifications (for receipts subscription)
// [from,to)
func (r *RecentReceipts) NotifyReceipts(n *Events, from, to uint64, isUnwind bool) {
	if !n.HasReceiptSubscriptions() {
		return
	}
	r.mu.Lock()
	defer r.mu.Unlock()

	for blockNum, receipts := range r.receipts {
		if blockNum+r.limit < from { // evict old
			delete(r.receipts, blockNum)
			delete(r.txs, blockNum)
			delete(r.headers, blockNum)
			continue
		}
		if blockNum < from || blockNum >= to {
			continue
		}

		txs := r.txs[blockNum]
		header := r.headers[blockNum]
		if len(receipts) == 0 || len(txs) == 0 || header == nil {
			continue
		}

		var reply []*remoteproto.SubscribeReceiptsReply
		signer := types.MakeSigner(nil, blockNum, 0)

		for _, receipt := range receipts {
			if receipt == nil {
				continue
			}

			txIndex := receipt.TransactionIndex
			if int(txIndex) >= len(txs) {
				continue
			}
			txn := txs[txIndex]

			// Convert logs to Proto format
			protoLogs := make([]*remoteproto.SubscribeLogsReply, 0, len(receipt.Logs))
			for _, l := range receipt.Logs {
				protoLog := &remoteproto.SubscribeLogsReply{
					Address:          gointerfaces.ConvertAddressToH160(l.Address),
					BlockHash:        gointerfaces.ConvertHashToH256(receipt.BlockHash),
					BlockNumber:      blockNum,
					Data:             l.Data,
					LogIndex:         uint64(l.Index),
					Topics:           make([]*typesproto.H256, 0, len(l.Topics)),
					TransactionHash:  gointerfaces.ConvertHashToH256(receipt.TxHash),
					TransactionIndex: uint64(l.TxIndex),
					Removed:          isUnwind,
				}
				for _, topic := range l.Topics {
					protoLog.Topics = append(protoLog.Topics, gointerfaces.ConvertHashToH256(topic))
				}
				protoLogs = append(protoLogs, protoLog)
			}

			// Build Proto receipt with all metadata
			protoReceipt := &remoteproto.SubscribeReceiptsReply{
				BlockHash:         gointerfaces.ConvertHashToH256(receipt.BlockHash),
				BlockNumber:       blockNum,
				TransactionHash:   gointerfaces.ConvertHashToH256(receipt.TxHash),
				TransactionIndex:  uint64(txIndex),
				Type:              uint32(receipt.Type),
				Status:            receipt.Status,
				CumulativeGasUsed: receipt.CumulativeGasUsed,
				GasUsed:           receipt.GasUsed,
				LogsBloom:         receipt.Bloom[:],
				Logs:              protoLogs,
				BlobGasUsed:       receipt.BlobGasUsed,
			}

			// Add contract address if present
			if receipt.ContractAddress != (common.Address{}) {
				protoReceipt.ContractAddress = gointerfaces.ConvertAddressToH160(receipt.ContractAddress)
			}

			// Add transaction data (from/to) from txs array
			if sender, err := txn.Sender(*signer); err == nil {
				protoReceipt.From = gointerfaces.ConvertAddressToH160(sender.Value())
			}
			if to := txn.GetTo(); to != nil {
				protoReceipt.To = gointerfaces.ConvertAddressToH160(*to)
			}
			protoReceipt.TxType = uint32(txn.Type())

			// Add header data
			if header.BaseFee != nil {
				protoReceipt.BaseFee = gointerfaces.ConvertUint256IntToH256(header.BaseFee)
			}
			protoReceipt.BlockTime = header.Time
			if header.ExcessBlobGas != nil {
				protoReceipt.ExcessBlobGas = *header.ExcessBlobGas
			}

			// Add blob gas price for EIP-4844 if needed
			// Can be calculated from ExcessBlobGas

			reply = append(reply, protoReceipt)
		}

		// Send batch per block
		if len(reply) > 0 {
			n.OnReceipts(reply)
		}
	}
}

func (r *RecentReceipts) CopyAndReset(target *RecentReceipts) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for blockNum, receipts := range r.receipts {
		txs := r.txs[blockNum]
		header := r.headers[blockNum]
		target.Add(receipts, txs, header)
		delete(r.receipts, blockNum)
		delete(r.txs, blockNum)
		delete(r.headers, blockNum)
	}
}
