package lite

import (
	"github.com/tendermint/tendermint/types"

	liteErr "github.com/tendermint/tendermint/lite/errors"
)

var _ Certifier = (*DynamicCertifier)(nil)

// DynamicCertifier uses a StaticCertifier for Certify, but adds an
// Update method to allow for a change of validators.
//
// You can pass in a FullCommit with another validator set,
// and if this is a provably secure transition (< 1/3 change,
// sufficient signatures), then it will update the
// validator set for the next Certify call.
// For security, it will only follow validator set changes
// going forward.
type DynamicCertifier struct {
	cert       *StaticCertifier
	lastHeight int64
}

// NewDynamic returns a new dynamic certifier.
func NewDynamicCertifier(chainID string, vals *types.ValidatorSet, height int64) *DynamicCertifier {
	return &DynamicCertifier{
		cert:       NewStaticCertifier(chainID, vals),
		lastHeight: height,
	}
}

// ChainID returns the chain id of this certifier.
// Implements Certifier.
func (dc *DynamicCertifier) ChainID() string {
	return dc.cert.ChainID()
}

// Validators returns the validators of this certifier.
func (dc *DynamicCertifier) Validators() *types.ValidatorSet {
	return dc.cert.vSet
}

// Hash returns the hash of this certifier.
func (dc *DynamicCertifier) Hash() []byte {
	return dc.cert.Hash()
}

// LastHeight returns the last height of this certifier.
func (dc *DynamicCertifier) LastHeight() int64 {
	return dc.lastHeight
}

// Certify will verify whether the commit is valid and will update the height if it is or return an
// error if it is not.
// Implements Certifier.
func (dc *DynamicCertifier) Certify(check Commit) error {
	err := dc.cert.Certify(check)
	if err == nil {
		// update last seen height if input is valid
		dc.lastHeight = check.Height()
	}
	return err
}

// Update will verify if this is a valid change and update
// the certifying validator set if safe to do so.
//
// Returns an error if update is impossible (invalid proof or IsTooMuchChangeErr)
func (dc *DynamicCertifier) Update(fc FullCommit) error {
	// ignore all checkpoints in the past -> only to the future
	h := fc.Height()
	if h <= dc.lastHeight {
		return liteErr.ErrPastTime()
	}

	// first, verify if the input is self-consistent....
	err := fc.ValidateBasic(dc.ChainID())
	if err != nil {
		return err
	}

	// now, make sure not too much change... meaning this commit
	// would be approved by the currently known validator set
	// as well as the new set
	commit := fc.Commit.Commit
	err = dc.Validators().VerifyCommitAny(fc.Validators, dc.ChainID(), commit.BlockID, h, commit)
	if err != nil {
		return liteErr.ErrTooMuchChange()
	}

	// looks good, we can update
	dc.cert = NewStaticCertifier(dc.ChainID(), fc.Validators)
	dc.lastHeight = h
	return nil
}
