// Copyright 2014 The go-ethereum Authors
// (original work)
// Copyright 2024 The Erigon Authors
// (modifications)
// 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 types

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"slices"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/hexutil"
	"github.com/erigontech/erigon/execution/rlp"
)

// Log represents a contract log event. These events are generated by the LOG opcode and
// stored/indexed by the node.
type Log struct {
	// Consensus fields:
	// address of the contract that generated the event
	Address common.Address `json:"address" codec:"1"`
	// list of topics provided by the contract.
	Topics []common.Hash `json:"topics" codec:"2"`
	// supplied by the contract, usually ABI-encoded
	Data hexutil.Bytes `json:"data" codec:"3"`

	// Derived fields. These fields are filled in by the node
	// but not secured by consensus.
	// block in which the transaction was included
	BlockNumber hexutil.Uint64 `json:"blockNumber" codec:"-"`

	// hash of the transaction
	TxHash common.Hash `json:"transactionHash" codec:"-"`
	// index of the transaction in the block
	TxIndex hexutil.Uint `json:"transactionIndex" codec:"-"`
	// hash of the block in which the transaction was included
	BlockHash common.Hash `json:"blockHash" codec:"-"`
	// index of the log in the block
	Index hexutil.Uint `json:"logIndex" codec:"-"`

	// The Removed field is true if this log was reverted due to a chain reorganisation.
	// You must pay attention to this field if you receive logs through a filter query.
	Removed bool `json:"removed" codec:"-"`
}

// UnmarshalJSON validates required fields: address, topics, data, transactionHash.
func (l *Log) UnmarshalJSON(input []byte) error {
	type log struct {
		Address     *common.Address `json:"address"`
		Topics      *[]common.Hash  `json:"topics"`
		Data        *hexutil.Bytes  `json:"data"`
		BlockNumber *hexutil.Uint64 `json:"blockNumber"`
		TxHash      *common.Hash    `json:"transactionHash"`
		TxIndex     *hexutil.Uint   `json:"transactionIndex"`
		BlockHash   *common.Hash    `json:"blockHash"`
		Index       *hexutil.Uint   `json:"logIndex"`
		Removed     *bool           `json:"removed"`
	}
	var dec log
	if err := json.Unmarshal(input, &dec); err != nil {
		return err
	}
	if dec.Address == nil {
		return errors.New("missing required field 'address' for Log")
	}
	l.Address = *dec.Address
	if dec.Topics == nil {
		return errors.New("missing required field 'topics' for Log")
	}
	l.Topics = *dec.Topics
	if dec.Data == nil {
		return errors.New("missing required field 'data' for Log")
	}
	l.Data = *dec.Data
	if dec.TxHash == nil {
		return errors.New("missing required field 'transactionHash' for Log")
	}
	l.TxHash = *dec.TxHash
	if dec.BlockNumber != nil {
		l.BlockNumber = *dec.BlockNumber
	}
	if dec.TxIndex != nil {
		l.TxIndex = *dec.TxIndex
	}
	if dec.BlockHash != nil {
		l.BlockHash = *dec.BlockHash
	}
	if dec.Index != nil {
		l.Index = *dec.Index
	}
	if dec.Removed != nil {
		l.Removed = *dec.Removed
	}
	return nil
}

// UnmarshalJSON validates required fields and parses the Timestamp field.
func (l *ErigonLog) UnmarshalJSON(input []byte) error {
	type flat struct {
		Address     *common.Address `json:"address"`
		Topics      *[]common.Hash  `json:"topics"`
		Data        *hexutil.Bytes  `json:"data"`
		BlockNumber *hexutil.Uint64 `json:"blockNumber"`
		TxHash      *common.Hash    `json:"transactionHash"`
		TxIndex     *hexutil.Uint   `json:"transactionIndex"`
		BlockHash   *common.Hash    `json:"blockHash"`
		Index       *hexutil.Uint   `json:"logIndex"`
		Removed     *bool           `json:"removed"`
		Timestamp   *hexutil.Uint64 `json:"timestamp"`
	}
	var dec flat
	if err := json.Unmarshal(input, &dec); err != nil {
		return err
	}
	if dec.Address == nil {
		return errors.New("missing required field 'address' for Log")
	}
	l.Address = *dec.Address
	if dec.Topics == nil {
		return errors.New("missing required field 'topics' for Log")
	}
	l.Topics = *dec.Topics
	if dec.Data == nil {
		return errors.New("missing required field 'data' for Log")
	}
	l.Data = *dec.Data
	if dec.TxHash == nil {
		return errors.New("missing required field 'transactionHash' for Log")
	}
	l.TxHash = *dec.TxHash
	if dec.BlockNumber != nil {
		l.BlockNumber = *dec.BlockNumber
	}
	if dec.TxIndex != nil {
		l.TxIndex = *dec.TxIndex
	}
	if dec.BlockHash != nil {
		l.BlockHash = *dec.BlockHash
	}
	if dec.Index != nil {
		l.Index = *dec.Index
	}
	if dec.Removed != nil {
		l.Removed = *dec.Removed
	}
	if dec.Timestamp != nil {
		l.Timestamp = *dec.Timestamp
	}
	return nil
}

type Logs []*Log

type ErigonLog struct {
	Log
	Timestamp hexutil.Uint64 `json:"timestamp" codec:"-"`
}

type ErigonLogs []*ErigonLog

// ToErigonLogs converts Logs to ErigonLogs, adding a timestamp to each entry.
func (logs Logs) ToErigonLogs(timestamp uint64) ErigonLogs {
	result := make(ErigonLogs, len(logs))
	for i, l := range logs {
		result[i] = &ErigonLog{
			Log:       *l,
			Timestamp: hexutil.Uint64(timestamp),
		}
	}
	return result
}

// RPCLog Extends `types.Log` and add BlockTimestamp field
type RPCLog struct {
	Log
	BlockTimestamp hexutil.Uint64 `json:"blockTimestamp" codec:"-"`
}

// UnmarshalJSON parses both the embedded Log fields and the RPC-specific blockTimestamp field.
func (l *RPCLog) UnmarshalJSON(input []byte) error {
	type flat struct {
		Address        *common.Address `json:"address"`
		Topics         *[]common.Hash  `json:"topics"`
		Data           *hexutil.Bytes  `json:"data"`
		BlockNumber    *hexutil.Uint64 `json:"blockNumber"`
		TxHash         *common.Hash    `json:"transactionHash"`
		TxIndex        *hexutil.Uint   `json:"transactionIndex"`
		BlockHash      *common.Hash    `json:"blockHash"`
		Index          *hexutil.Uint   `json:"logIndex"`
		Removed        *bool           `json:"removed"`
		BlockTimestamp *hexutil.Uint64 `json:"blockTimestamp"`
	}
	var dec flat
	if err := json.Unmarshal(input, &dec); err != nil {
		return err
	}
	if dec.Address == nil {
		return errors.New("missing required field 'address' for Log")
	}
	l.Address = *dec.Address
	if dec.Topics == nil {
		return errors.New("missing required field 'topics' for Log")
	}
	l.Topics = *dec.Topics
	if dec.Data == nil {
		return errors.New("missing required field 'data' for Log")
	}
	l.Data = *dec.Data
	if dec.TxHash == nil {
		return errors.New("missing required field 'transactionHash' for Log")
	}
	l.TxHash = *dec.TxHash
	if dec.BlockNumber != nil {
		l.BlockNumber = *dec.BlockNumber
	}
	if dec.TxIndex != nil {
		l.TxIndex = *dec.TxIndex
	}
	if dec.BlockHash != nil {
		l.BlockHash = *dec.BlockHash
	}
	if dec.Index != nil {
		l.Index = *dec.Index
	}
	if dec.Removed != nil {
		l.Removed = *dec.Removed
	}
	if dec.BlockTimestamp != nil {
		l.BlockTimestamp = *dec.BlockTimestamp
	}
	return nil
}

type RPCLogs []*RPCLog

func (logs Logs) Copy() Logs {
	if logs == nil {
		return nil
	}
	logsCopy := make(Logs, len(logs))
	for i, log := range logs {
		logsCopy[i] = log.Copy()
	}
	return logsCopy
}

// ToRPCTransactionLog converts types.Log in a RPCLog.
func ToRPCTransactionLog(log *Log, header *Header, txHash common.Hash, txIndex uint64) *RPCLog {
	return &RPCLog{
		Log:            *log,
		BlockTimestamp: hexutil.Uint64(header.Time),
	}
}

// BuildTopicMap converts a slice-of-alternatives representation of topics into a
// pre-built lookup map suitable for repeated calls to FilterWithTopicMap.
func BuildTopicMap(topics [][]common.Hash) []map[common.Hash]struct{} {
	topicMap := make([]map[common.Hash]struct{}, len(topics))
	for idx, v := range topics {
		if len(v) == 0 {
			continue // nil entry = wildcard; FilterWithTopicMap treats len(nil)==0 as match-all
		}
		topicMap[idx] = make(map[common.Hash]struct{}, len(v))
		for _, h := range v {
			topicMap[idx][h] = struct{}{}
		}
	}
	return topicMap
}

// FilterWithTopicMap filters logs using a pre-built topic map. Use this when filtering
// in a loop with the same topics to avoid rebuilding the map on every call.
func (logs Logs) FilterWithTopicMap(addrMap map[common.Address]struct{}, topicMap []map[common.Hash]struct{}, maxLogs uint64) Logs {
	o := make(Logs, 0, len(logs))
	var logCount uint64
	for _, v := range logs {
		if len(addrMap) != 0 {
			if _, ok := addrMap[v.Address]; !ok {
				continue
			}
		}
		if len(topicMap) > len(v.Topics) {
			continue
		}
		found := true
		for idx, topicSet := range topicMap {
			if len(topicSet) == 0 {
				continue
			}
			if _, ok := topicSet[v.Topics[idx]]; !ok {
				found = false
				break
			}
		}
		if found {
			o = append(o, v)
		}
		logCount++
		if maxLogs != 0 && logCount >= maxLogs {
			break
		}
	}
	return o
}

func (logs Logs) Filter(addrMap map[common.Address]struct{}, topics [][]common.Hash, maxLogs uint64) Logs {
	return logs.FilterWithTopicMap(addrMap, BuildTopicMap(topics), maxLogs)
}

func (logs Logs) ContainingTopics(addrMap map[common.Address]struct{}, topicsMap map[common.Hash]struct{}, maxLogs uint64) Logs {
	o := make(Logs, 0, len(logs))
	var logCount uint64

	for _, v := range logs {
		found := false

		// check address if addrMap is not empty
		if _, requested := addrMap[v.Address]; !requested && len(addrMap) > 0 {
			continue // not there? skip this log
		}
		//topicsMap len zero match any topics
		if len(topicsMap) == 0 {
			o = append(o, v)
		} else {
			for i := range v.Topics {
				//Contain any topics that matched
				if _, ok := topicsMap[v.Topics[i]]; ok {
					found = true
				}
			}
			if found {
				o = append(o, v)
			}
		}
		logCount += 1
		if maxLogs != 0 && logCount >= maxLogs {
			break
		}
	}
	return o
}

type rlpLog struct {
	Address common.Address
	Topics  []common.Hash
	Data    []byte
}

type rlpStorageLog struct {
	Address common.Address
	Topics  []common.Hash
	Data    []byte
	//BlockNumber uint64
	//TxHash common.Hash
	//TxIndex uint
	//BlockHash   common.Hash
	//Index uint
}

// EncodeRLP implements rlp.Encoder.
func (l *Log) EncodeRLP(w io.Writer) error {
	return rlp.Encode(w, rlpLog{Address: l.Address, Topics: l.Topics, Data: l.Data})
}

// DecodeRLP implements rlp.Decoder.
func (l *Log) DecodeRLP(s *rlp.Stream) error {
	var dec rlpLog
	err := s.Decode(&dec)
	if err == nil {
		l.Address, l.Topics, l.Data = dec.Address, dec.Topics, dec.Data
	}
	return err
}

// Copy creates a deep copy of the Log.
func (l *Log) Copy() *Log {
	if l == nil {
		return nil
	}
	return &Log{
		Address:     l.Address,
		Topics:      slices.Clone(l.Topics),
		Data:        slices.Clone(l.Data),
		BlockNumber: l.BlockNumber,
		TxHash:      l.TxHash,
		TxIndex:     l.TxIndex,
		BlockHash:   l.BlockHash,
		Index:       l.Index,
		Removed:     l.Removed,
	}
}

// LogForStorage is a wrapper around a Log that flattens and parses the entire content of
// a log including non-consensus fields.
type LogForStorage Log

// EncodeRLP implements rlp.Encoder.
func (l *LogForStorage) EncodeRLP(w io.Writer) error {
	return rlp.Encode(w, rlpStorageLog{
		Address: l.Address,
		Topics:  l.Topics,
		Data:    l.Data,
	})
}

func decodeTopics2(s *rlp.Stream) (list []common.Hash, err error) {
	l, err := s.List()
	if err != nil {
		return nil, err
	}
	if l == 0 {
		return []common.Hash{}, s.ListEnd()
	}
	listLen := int(l / (1 + 32))  // rlpLenPrefix+32bytes
	preAlloc := min(128, listLen) // attacker may craft rlp prefix - which will trigger hube pre-alloc. so, add hard-limit
	list = make([]common.Hash, 0, preAlloc)
	var i int
	var b common.Hash
	for ; s.MoreDataInList(); i++ {
		if err = s.ReadBytes(b[:]); err != nil {
			return nil, err
		}
		list = append(list, b)
	}
	return list[:i], s.ListEnd()
}

// DecodeRLP implements rlp.Decoder.
//
// Note some redundant fields(e.g. block number, txn hash etc) will be assembled later.
func (l *LogForStorage) DecodeRLP(s *rlp.Stream) error {
	_, err := s.List()
	if err != nil {
		return err
	}
	err = s.ReadBytes(l.Address[:])
	if err != nil {
		return fmt.Errorf("read Address: %w", err)
	}
	l.Topics, err = decodeTopics2(s)
	if err != nil {
		return err
	}
	if l.Data, err = s.Bytes(); err != nil {
		return fmt.Errorf("read Data: %w", err)
	}
	return s.ListEnd()
}
