package lite

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

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

var _ Certifier = (*InquiringCertifier)(nil)

// InquiringCertifier wraps a dynamic certifier and implements an auto-update strategy. If a call
// to Certify fails due to a change it validator set, InquiringCertifier will try and find a
// previous FullCommit which it can use to safely update the validator set. It uses a source
// provider to obtain the needed FullCommits. It stores properly validated data on the local system.
type InquiringCertifier struct {
	cert *DynamicCertifier
	// These are only properly validated data, from local system
	trusted Provider
	// This is a source of new info, like a node rpc, or other import method
	Source Provider
}

// NewInquiringCertifier returns a new Inquiring object. It uses the trusted provider to store
// validated data and the source provider to obtain missing FullCommits.
//
// Example: The trusted provider should a CacheProvider, MemProvider or files.Provider. The source
// provider should be a client.HTTPProvider.
func NewInquiringCertifier(chainID string, fc FullCommit, trusted Provider,
	source Provider) (*InquiringCertifier, error) {

	// store the data in trusted
	err := trusted.StoreCommit(fc)
	if err != nil {
		return nil, err
	}

	return &InquiringCertifier{
		cert:    NewDynamicCertifier(chainID, fc.Validators, fc.Height()),
		trusted: trusted,
		Source:  source,
	}, nil
}

// ChainID returns the chain id.
// Implements Certifier.
func (ic *InquiringCertifier) ChainID() string {
	return ic.cert.ChainID()
}

// Validators returns the validator set.
func (ic *InquiringCertifier) Validators() *types.ValidatorSet {
	return ic.cert.cert.vSet
}

// LastHeight returns the last height.
func (ic *InquiringCertifier) LastHeight() int64 {
	return ic.cert.lastHeight
}

// Certify makes sure this is checkpoint is valid.
//
// If the validators have changed since the last know time, it looks
// for a path to prove the new validators.
//
// On success, it will store the checkpoint in the store for later viewing
// Implements Certifier.
func (ic *InquiringCertifier) Certify(commit Commit) error {
	err := ic.useClosestTrust(commit.Height())
	if err != nil {
		return err
	}

	err = ic.cert.Certify(commit)
	if !liteErr.IsValidatorsChangedErr(err) {
		return err
	}
	err = ic.updateToHash(commit.Header.ValidatorsHash)
	if err != nil {
		return err
	}

	err = ic.cert.Certify(commit)
	if err != nil {
		return err
	}

	// store the new checkpoint
	return ic.trusted.StoreCommit(NewFullCommit(commit, ic.Validators()))
}

// Update will verify if this is a valid change and update
// the certifying validator set if safe to do so.
func (ic *InquiringCertifier) Update(fc FullCommit) error {
	err := ic.useClosestTrust(fc.Height())
	if err != nil {
		return err
	}

	err = ic.cert.Update(fc)
	if err == nil {
		err = ic.trusted.StoreCommit(fc)
	}
	return err
}

func (ic *InquiringCertifier) useClosestTrust(h int64) error {
	closest, err := ic.trusted.GetByHeight(h)
	if err != nil {
		return err
	}

	// if the best seed is not the one we currently use,
	// let's just reset the dynamic validator
	if closest.Height() != ic.LastHeight() {
		ic.cert = NewDynamicCertifier(ic.ChainID(), closest.Validators, closest.Height())
	}
	return nil
}

// updateToHash gets the validator hash we want to update to
// if IsTooMuchChangeErr, we try to find a path by binary search over height
func (ic *InquiringCertifier) updateToHash(vhash []byte) error {
	// try to get the match, and update
	fc, err := ic.Source.GetByHash(vhash)
	if err != nil {
		return err
	}
	err = ic.cert.Update(fc)
	// handle IsTooMuchChangeErr by using divide and conquer
	if liteErr.IsTooMuchChangeErr(err) {
		err = ic.updateToHeight(fc.Height())
	}
	return err
}

// updateToHeight will use divide-and-conquer to find a path to h
func (ic *InquiringCertifier) updateToHeight(h int64) error {
	// try to update to this height (with checks)
	fc, err := ic.Source.GetByHeight(h)
	if err != nil {
		return err
	}
	start, end := ic.LastHeight(), fc.Height()
	if end <= start {
		return liteErr.ErrNoPathFound()
	}
	err = ic.Update(fc)

	// we can handle IsTooMuchChangeErr specially
	if !liteErr.IsTooMuchChangeErr(err) {
		return err
	}

	// try to update to mid
	mid := (start + end) / 2
	err = ic.updateToHeight(mid)
	if err != nil {
		return err
	}

	// if we made it to mid, we recurse
	return ic.updateToHeight(h)
}
