/*
Package client defines a provider that uses a rpcclient
to get information, which is used to get new headers
and validators directly from a node.
*/
package client

import (
	"bytes"

	rpcclient "github.com/tendermint/tendermint/rpc/client"
	ctypes "github.com/tendermint/tendermint/rpc/core/types"
	"github.com/tendermint/tendermint/types"

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

// SignStatusClient combines a SignClient and StatusClient.
type SignStatusClient interface {
	rpcclient.SignClient
	rpcclient.StatusClient
}

type provider struct {
	node       SignStatusClient
	lastHeight int64
}

// NewProvider can wrap any rpcclient to expose it as
// a read-only provider.
func NewProvider(node SignStatusClient) lite.Provider {
	return &provider{node: node}
}

// NewHTTPProvider can connect to a tendermint json-rpc endpoint
// at the given url, and uses that as a read-only provider.
func NewHTTPProvider(remote string) lite.Provider {
	return &provider{
		node: rpcclient.NewHTTP(remote, "/websocket"),
	}
}

// StatusClient returns the internal node as a StatusClient
func (p *provider) StatusClient() rpcclient.StatusClient {
	return p.node
}

// StoreCommit is a noop, as clients can only read from the chain...
func (p *provider) StoreCommit(_ lite.FullCommit) error { return nil }

// GetHash gets the most recent validator and sees if it matches
//
// TODO: improve when the rpc interface supports more functionality
func (p *provider) GetByHash(hash []byte) (lite.FullCommit, error) {
	var fc lite.FullCommit
	vals, err := p.node.Validators(nil)
	// if we get no validators, or a different height, return an error
	if err != nil {
		return fc, err
	}
	p.updateHeight(vals.BlockHeight)
	vhash := types.NewValidatorSet(vals.Validators).Hash()
	if !bytes.Equal(hash, vhash) {
		return fc, liteErr.ErrCommitNotFound()
	}
	return p.seedFromVals(vals)
}

// GetByHeight gets the validator set by height
func (p *provider) GetByHeight(h int64) (fc lite.FullCommit, err error) {
	commit, err := p.node.Commit(&h)
	if err != nil {
		return fc, err
	}
	return p.seedFromCommit(commit)
}

// LatestCommit returns the newest commit stored.
func (p *provider) LatestCommit() (fc lite.FullCommit, err error) {
	commit, err := p.GetLatestCommit()
	if err != nil {
		return fc, err
	}
	return p.seedFromCommit(commit)
}

// GetLatestCommit should return the most recent commit there is,
// which handles queries for future heights as per the semantics
// of GetByHeight.
func (p *provider) GetLatestCommit() (*ctypes.ResultCommit, error) {
	status, err := p.node.Status()
	if err != nil {
		return nil, err
	}
	return p.node.Commit(&status.SyncInfo.LatestBlockHeight)
}

// CommitFromResult ...
func CommitFromResult(result *ctypes.ResultCommit) lite.Commit {
	return (lite.Commit)(result.SignedHeader)
}

func (p *provider) seedFromVals(vals *ctypes.ResultValidators) (lite.FullCommit, error) {
	// now get the commits and build a full commit
	commit, err := p.node.Commit(&vals.BlockHeight)
	if err != nil {
		return lite.FullCommit{}, err
	}
	fc := lite.NewFullCommit(
		CommitFromResult(commit),
		types.NewValidatorSet(vals.Validators),
	)
	return fc, nil
}

func (p *provider) seedFromCommit(commit *ctypes.ResultCommit) (fc lite.FullCommit, err error) {
	fc.Commit = CommitFromResult(commit)

	// now get the proper validators
	vals, err := p.node.Validators(&commit.Header.Height)
	if err != nil {
		return fc, err
	}

	// make sure they match the commit (as we cannot enforce height)
	vset := types.NewValidatorSet(vals.Validators)
	if !bytes.Equal(vset.Hash(), commit.Header.ValidatorsHash) {
		return fc, liteErr.ErrValidatorsChanged()
	}

	p.updateHeight(commit.Header.Height)
	fc.Validators = vset
	return fc, nil
}

func (p *provider) updateHeight(h int64) {
	if h > p.lastHeight {
		p.lastHeight = h
	}
}
