// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library 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.
//
// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package light

import (
	"errors"
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/ethereum/go-ethereum/beacon/params"
	"github.com/ethereum/go-ethereum/beacon/types"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/lru"
	"github.com/ethereum/go-ethereum/common/mclock"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/log"
)

var (
	ErrNeedCommittee      = errors.New("sync committee required")
	ErrInvalidUpdate      = errors.New("invalid committee update")
	ErrInvalidPeriod      = errors.New("invalid update period")
	ErrWrongCommitteeRoot = errors.New("wrong committee root")
	ErrCannotReorg        = errors.New("can not reorg committee chain")
)

// CommitteeChain is a passive data structure that can validate, hold and update
// a chain of beacon light sync committees and updates. It requires at least one
// externally set fixed committee root at the beginning of the chain which can
// be set either based on a BootstrapData or a trusted source (a local beacon
// full node). This makes the structure useful for both light client and light
// server setups.
//
// It always maintains the following consistency constraints:
//   - a committee can only be present if its root hash matches an existing fixed
//     root or if it is proven by an update at the previous period
//   - an update can only be present if a committee is present at the same period
//     and the update signature is valid and has enough participants.
//     The committee at the next period (proven by the update) should also be
//     present (note that this means they can only be added together if neither
//     is present yet). If a fixed root is present at the next period then the
//     update can only be present if it proves the same committee root.
//
// Once synced to the current sync period, CommitteeChain can also validate
// signed beacon headers.
type CommitteeChain struct {
	// chainmu guards against concurrent access to the canonicalStore structures
	// (updates, committees, fixedCommitteeRoots) and ensures that they stay consistent
	// with each other and with committeeCache.
	chainmu             sync.RWMutex
	db                  ethdb.KeyValueStore
	updates             *canonicalStore[*types.LightClientUpdate]
	committees          *canonicalStore[*types.SerializedSyncCommittee]
	fixedCommitteeRoots *canonicalStore[common.Hash]
	committeeCache      *lru.Cache[uint64, syncCommittee] // cache deserialized committees
	changeCounter       uint64

	clock       mclock.Clock         // monotonic clock (simulated clock in tests)
	unixNano    func() int64         // system clock (simulated clock in tests)
	sigVerifier committeeSigVerifier // BLS sig verifier (dummy verifier in tests)

	config             *params.ChainConfig
	minimumUpdateScore types.UpdateScore
	enforceTime        bool // enforceTime specifies whether the age of a signed header should be checked
}

// NewCommitteeChain creates a new CommitteeChain.
func NewCommitteeChain(db ethdb.KeyValueStore, config *params.ChainConfig, signerThreshold int, enforceTime bool) *CommitteeChain {
	return newCommitteeChain(db, config, signerThreshold, enforceTime, blsVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
}

// NewTestCommitteeChain creates a new CommitteeChain for testing.
func NewTestCommitteeChain(db ethdb.KeyValueStore, config *params.ChainConfig, signerThreshold int, enforceTime bool, clock *mclock.Simulated) *CommitteeChain {
	return newCommitteeChain(db, config, signerThreshold, enforceTime, dummyVerifier{}, clock, func() int64 { return int64(clock.Now()) })
}

// newCommitteeChain creates a new CommitteeChain with the option of replacing the
// clock source and signature verification for testing purposes.
func newCommitteeChain(db ethdb.KeyValueStore, config *params.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain {
	s := &CommitteeChain{
		committeeCache: lru.NewCache[uint64, syncCommittee](10),
		db:             db,
		sigVerifier:    sigVerifier,
		clock:          clock,
		unixNano:       unixNano,
		config:         config,
		enforceTime:    enforceTime,
		minimumUpdateScore: types.UpdateScore{
			SignerCount:    uint32(signerThreshold),
			SubPeriodIndex: params.SyncPeriodLength / 16,
		},
	}

	var err1, err2, err3 error
	if s.fixedCommitteeRoots, err1 = newCanonicalStore[common.Hash](db, rawdb.FixedCommitteeRootKey); err1 != nil {
		log.Error("Error creating fixed committee root store", "error", err1)
	}
	if s.committees, err2 = newCanonicalStore[*types.SerializedSyncCommittee](db, rawdb.SyncCommitteeKey); err2 != nil {
		log.Error("Error creating committee store", "error", err2)
	}
	if s.updates, err3 = newCanonicalStore[*types.LightClientUpdate](db, rawdb.BestUpdateKey); err3 != nil {
		log.Error("Error creating update store", "error", err3)
	}
	if err1 != nil || err2 != nil || err3 != nil || !s.checkConstraints() {
		log.Info("Resetting invalid committee chain")
		s.Reset()
	}
	// roll back invalid updates (might be necessary if forks have been changed since last time)
	for !s.updates.periods.isEmpty() {
		update, ok := s.updates.get(s.db, s.updates.periods.End-1)
		if !ok {
			log.Error("Sync committee update missing", "period", s.updates.periods.End-1)
			s.Reset()
			break
		}
		if valid, err := s.verifyUpdate(update); err != nil {
			log.Error("Error validating update", "period", s.updates.periods.End-1, "error", err)
		} else if valid {
			break
		}
		if err := s.rollback(s.updates.periods.End); err != nil {
			log.Error("Error writing batch into chain database", "error", err)
		}
	}
	if !s.committees.periods.isEmpty() {
		log.Trace("Sync committee chain loaded", "first period", s.committees.periods.Start, "last period", s.committees.periods.End-1)
	}
	return s
}

// checkConstraints checks committee chain validity constraints
func (s *CommitteeChain) checkConstraints() bool {
	isNotInFixedCommitteeRootRange := func(r periodRange) bool {
		return s.fixedCommitteeRoots.periods.isEmpty() ||
			r.Start < s.fixedCommitteeRoots.periods.Start ||
			r.Start >= s.fixedCommitteeRoots.periods.End
	}

	valid := true
	if !s.updates.periods.isEmpty() {
		if isNotInFixedCommitteeRootRange(s.updates.periods) {
			log.Error("Start update is not in the fixed roots range")
			valid = false
		}
		if s.committees.periods.Start > s.updates.periods.Start || s.committees.periods.End <= s.updates.periods.End {
			log.Error("Missing committees in update range")
			valid = false
		}
	}
	if !s.committees.periods.isEmpty() {
		if isNotInFixedCommitteeRootRange(s.committees.periods) {
			log.Error("Start committee is not in the fixed roots range")
			valid = false
		}
		if s.committees.periods.End > s.fixedCommitteeRoots.periods.End && s.committees.periods.End > s.updates.periods.End+1 {
			log.Error("Last committee is neither in the fixed roots range nor proven by updates")
			valid = false
		}
	}
	return valid
}

// Reset resets the committee chain.
func (s *CommitteeChain) Reset() {
	s.chainmu.Lock()
	defer s.chainmu.Unlock()

	if err := s.rollback(0); err != nil {
		log.Error("Error writing batch into chain database", "error", err)
	}
	s.changeCounter++
}

// CheckpointInit initializes a CommitteeChain based on a checkpoint.
// Note: if the chain is already initialized and the committees proven by the
// checkpoint do match the existing chain then the chain is retained and the
// new checkpoint becomes fixed.
func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error {
	s.chainmu.Lock()
	defer s.chainmu.Unlock()

	if err := bootstrap.Validate(); err != nil {
		return err
	}
	period := bootstrap.Header.SyncPeriod()
	if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil {
		s.Reset()
		return err
	}
	if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil {
		s.Reset()
		if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil {
			s.Reset()
			return err
		}
	}
	if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil {
		s.Reset()
		return err
	}
	if err := s.addCommittee(period, bootstrap.Committee); err != nil {
		s.Reset()
		return err
	}
	s.changeCounter++
	return nil
}

// addFixedCommitteeRoot sets a fixed committee root at the given period.
// Note that the period where the first committee is added has to have a fixed
// root which can either come from a BootstrapData or a trusted source.
func (s *CommitteeChain) addFixedCommitteeRoot(period uint64, root common.Hash) error {
	if root == (common.Hash{}) {
		return ErrWrongCommitteeRoot
	}

	batch := s.db.NewBatch()
	oldRoot := s.getCommitteeRoot(period)
	if !s.fixedCommitteeRoots.periods.canExpand(period) {
		// Note: the fixed committee root range should always be continuous and
		// therefore the expected syncing method is to forward sync and optionally
		// backward sync periods one by one, starting from a checkpoint. The only
		// case when a root that is not adjacent to the already fixed ones can be
		// fixed is when the same root has already been proven by an update chain.
		// In this case the all roots in between can and should be fixed.
		// This scenario makes sense when a new trusted checkpoint is added to an
		// existing chain, ensuring that it will not be rolled back (might be
		// important in case of low signer participation rate).
		if root != oldRoot {
			return ErrInvalidPeriod
		}
		// if the old root exists and matches the new one then it is guaranteed
		// that the given period is after the existing fixed range and the roots
		// in between can also be fixed.
		for p := s.fixedCommitteeRoots.periods.End; p < period; p++ {
			if err := s.fixedCommitteeRoots.add(batch, p, s.getCommitteeRoot(p)); err != nil {
				return err
			}
		}
	}
	if oldRoot != (common.Hash{}) && (oldRoot != root) {
		// existing old root was different, we have to reorg the chain
		if err := s.rollback(period); err != nil {
			return err
		}
	}
	if err := s.fixedCommitteeRoots.add(batch, period, root); err != nil {
		return err
	}
	if err := batch.Write(); err != nil {
		log.Error("Error writing batch into chain database", "error", err)
		return err
	}
	return nil
}

// deleteFixedCommitteeRootsFrom deletes fixed roots starting from the given period.
// It also maintains chain consistency, meaning that it also deletes updates and
// committees if they are no longer supported by a valid update chain.
func (s *CommitteeChain) deleteFixedCommitteeRootsFrom(period uint64) error {
	if period >= s.fixedCommitteeRoots.periods.End {
		return nil
	}
	batch := s.db.NewBatch()
	s.fixedCommitteeRoots.deleteFrom(batch, period)
	if s.updates.periods.isEmpty() || period <= s.updates.periods.Start {
		// Note: the first period of the update chain should always be fixed so if
		// the fixed root at the first update is removed then the entire update chain
		// and the proven committees have to be removed. Earlier committees in the
		// remaining fixed root range can stay.
		s.updates.deleteFrom(batch, period)
		s.deleteCommitteesFrom(batch, period)
	} else {
		// The update chain stays intact, some previously fixed committee roots might
		// get unfixed but are still proven by the update chain. If there were
		// committees present after the range proven by updates, those should be
		// removed if the belonging fixed roots are also removed.
		fromPeriod := s.updates.periods.End + 1 // not proven by updates
		if period > fromPeriod {
			fromPeriod = period // also not justified by fixed roots
		}
		s.deleteCommitteesFrom(batch, fromPeriod)
	}
	if err := batch.Write(); err != nil {
		log.Error("Error writing batch into chain database", "error", err)
		return err
	}
	return nil
}

// deleteCommitteesFrom deletes committees starting from the given period.
func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) {
	deleted := s.committees.deleteFrom(batch, period)
	for period := deleted.Start; period < deleted.End; period++ {
		s.committeeCache.Remove(period)
	}
}

// addCommittee adds a committee at the given period if possible.
func (s *CommitteeChain) addCommittee(period uint64, committee *types.SerializedSyncCommittee) error {
	if !s.committees.periods.canExpand(period) {
		return ErrInvalidPeriod
	}
	root := s.getCommitteeRoot(period)
	if root == (common.Hash{}) {
		return ErrInvalidPeriod
	}
	if root != committee.Root() {
		return ErrWrongCommitteeRoot
	}
	if !s.committees.periods.contains(period) {
		if err := s.committees.add(s.db, period, committee); err != nil {
			return err
		}
		s.committeeCache.Remove(period)
	}
	return nil
}

// InsertUpdate adds a new update if possible.
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
	s.chainmu.Lock()
	defer s.chainmu.Unlock()

	period := update.AttestedHeader.Header.SyncPeriod()
	if !s.updates.periods.canExpand(period) || !s.committees.periods.contains(period) {
		return ErrInvalidPeriod
	}
	if s.minimumUpdateScore.BetterThan(update.Score()) {
		return ErrInvalidUpdate
	}
	oldRoot := s.getCommitteeRoot(period + 1)
	reorg := oldRoot != (common.Hash{}) && oldRoot != update.NextSyncCommitteeRoot
	if oldUpdate, ok := s.updates.get(s.db, period); ok && !update.Score().BetterThan(oldUpdate.Score()) {
		// a better or equal update already exists; no changes, only fail if new one tried to reorg
		if reorg {
			return ErrCannotReorg
		}
		return nil
	}
	if s.fixedCommitteeRoots.periods.contains(period+1) && reorg {
		return ErrCannotReorg
	}
	if ok, err := s.verifyUpdate(update); err != nil {
		return err
	} else if !ok {
		return ErrInvalidUpdate
	}
	addCommittee := !s.committees.periods.contains(period+1) || reorg
	if addCommittee {
		if nextCommittee == nil {
			return ErrNeedCommittee
		}
		if nextCommittee.Root() != update.NextSyncCommitteeRoot {
			return ErrWrongCommitteeRoot
		}
	}
	s.changeCounter++
	if reorg {
		if err := s.rollback(period + 1); err != nil {
			return err
		}
	}
	batch := s.db.NewBatch()
	if addCommittee {
		if err := s.committees.add(batch, period+1, nextCommittee); err != nil {
			return err
		}
		s.committeeCache.Remove(period + 1)
	}
	if err := s.updates.add(batch, period, update); err != nil {
		return err
	}
	if err := batch.Write(); err != nil {
		log.Error("Error writing batch into chain database", "error", err)
		return err
	}
	log.Info("Inserted new committee update", "period", period, "next committee root", update.NextSyncCommitteeRoot)
	return nil
}

// NextSyncPeriod returns the next period where an update can be added and also
// whether the chain is initialized at all.
func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
	s.chainmu.RLock()
	defer s.chainmu.RUnlock()

	if s.committees.periods.isEmpty() {
		return 0, false
	}
	if !s.updates.periods.isEmpty() {
		return s.updates.periods.End, true
	}
	return s.committees.periods.End - 1, true
}

func (s *CommitteeChain) ChangeCounter() uint64 {
	s.chainmu.RLock()
	defer s.chainmu.RUnlock()

	return s.changeCounter
}

// rollback removes all committees and fixed roots from the given period and updates
// starting from the previous period.
func (s *CommitteeChain) rollback(period uint64) error {
	max := s.updates.periods.End + 1
	if s.committees.periods.End > max {
		max = s.committees.periods.End
	}
	if s.fixedCommitteeRoots.periods.End > max {
		max = s.fixedCommitteeRoots.periods.End
	}
	for max > period {
		max--
		batch := s.db.NewBatch()
		s.deleteCommitteesFrom(batch, max)
		s.fixedCommitteeRoots.deleteFrom(batch, max)
		if max > 0 {
			s.updates.deleteFrom(batch, max-1)
		}
		if err := batch.Write(); err != nil {
			log.Error("Error writing batch into chain database", "error", err)
			return err
		}
	}
	return nil
}

// getCommitteeRoot returns the committee root at the given period, either fixed,
// proven by a previous update or both. It returns an empty hash if the committee
// root is unknown.
func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash {
	if root, ok := s.fixedCommitteeRoots.get(s.db, period); ok || period == 0 {
		return root
	}
	if update, ok := s.updates.get(s.db, period-1); ok {
		return update.NextSyncCommitteeRoot
	}
	return common.Hash{}
}

// getSyncCommittee returns the deserialized sync committee at the given period.
func (s *CommitteeChain) getSyncCommittee(period uint64) (syncCommittee, error) {
	if c, ok := s.committeeCache.Get(period); ok {
		return c, nil
	}
	if sc, ok := s.committees.get(s.db, period); ok {
		c, err := s.sigVerifier.deserializeSyncCommittee(sc)
		if err != nil {
			return nil, fmt.Errorf("sync committee #%d deserialization error: %v", period, err)
		}
		s.committeeCache.Add(period, c)
		return c, nil
	}
	return nil, fmt.Errorf("missing serialized sync committee #%d", period)
}

// VerifySignedHeader returns true if the given signed header has a valid signature
// according to the local committee chain. The caller should ensure that the
// committees advertised by the same source where the signed header came from are
// synced before verifying the signature.
// The age of the header is also returned (the time elapsed since the beginning
// of the given slot, according to the local system clock). If enforceTime is
// true then negative age (future) headers are rejected.
func (s *CommitteeChain) VerifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) {
	s.chainmu.RLock()
	defer s.chainmu.RUnlock()

	return s.verifySignedHeader(head)
}

func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) {
	var age time.Duration
	now := s.unixNano()
	if head.Header.Slot < (uint64(now-math.MinInt64)/uint64(time.Second)-s.config.GenesisTime)/12 {
		age = time.Duration(now - int64(time.Second)*int64(s.config.GenesisTime+head.Header.Slot*12))
	} else {
		age = time.Duration(math.MinInt64)
	}
	if s.enforceTime && age < 0 {
		return false, age, nil
	}
	committee, err := s.getSyncCommittee(types.SyncPeriod(head.SignatureSlot))
	if err != nil {
		return false, 0, err
	}
	if committee == nil {
		return false, age, nil
	}
	if signingRoot, err := s.config.Forks.SigningRoot(head.Header.Epoch(), head.Header.Hash()); err == nil {
		return s.sigVerifier.verifySignature(committee, signingRoot, &head.Signature), age, nil
	}
	return false, age, nil
}

// verifyUpdate checks whether the header signature is correct and the update
// fits into the specified constraints (assumes that the update has been
// successfully validated previously)
func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) (bool, error) {
	// Note: SignatureSlot determines the sync period of the committee used for signature
	// verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
	// setting them as equal here enforces the rule that they have to be in the same sync
	// period in order for the light client update proof to be meaningful.
	ok, age, err := s.verifySignedHeader(update.AttestedHeader)
	if age < 0 {
		log.Warn("Future committee update received", "age", age)
	}
	return ok, err
}
