package http

import (
	"context"
	"fmt"
	"math/rand"
	"regexp"
	"strings"
	"time"

	"github.com/tendermint/tendermint/light/provider"
	rpcclient "github.com/tendermint/tendermint/rpc/client"
	rpchttp "github.com/tendermint/tendermint/rpc/client/http"
	"github.com/tendermint/tendermint/types"
)

// This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740
var (
	regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`)
	maxRetryAttempts    = 10
)

// http provider uses an RPC client to obtain the necessary information.
type http struct {
	chainID string
	client  rpcclient.RemoteClient
}

// New creates a HTTP provider, which is using the rpchttp.HTTP client under
// the hood. If no scheme is provided in the remote URL, http will be used by
// default.
func New(chainID, remote string) (provider.Provider, error) {
	// Ensure URL scheme is set (default HTTP) when not provided.
	if !strings.Contains(remote, "://") {
		remote = "http://" + remote
	}

	httpClient, err := rpchttp.New(remote, "/websocket")
	if err != nil {
		return nil, err
	}

	return NewWithClient(chainID, httpClient), nil
}

// NewWithClient allows you to provide a custom client.
func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider {
	return &http{
		client:  client,
		chainID: chainID,
	}
}

func (p *http) String() string {
	return fmt.Sprintf("http{%s}", p.client.Remote())
}

// LightBlock fetches a LightBlock at the given height and checks the
// chainID matches.
func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
	h, err := validateHeight(height)
	if err != nil {
		return nil, provider.ErrBadLightBlock{Reason: err}
	}

	sh, err := p.signedHeader(ctx, h)
	if err != nil {
		return nil, err
	}

	vs, err := p.validatorSet(ctx, h)
	if err != nil {
		return nil, err
	}

	lb := &types.LightBlock{
		SignedHeader: sh,
		ValidatorSet: vs,
	}

	err = lb.ValidateBasic(p.chainID)
	if err != nil {
		return nil, provider.ErrBadLightBlock{Reason: err}
	}

	return lb, nil
}

// ReportEvidence calls `/broadcast_evidence` endpoint.
func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error {
	_, err := p.client.BroadcastEvidence(ctx, ev)
	return err
}

func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) {
	var (
		maxPerPage = 100
		vals       = []*types.Validator{}
		page       = 1
	)

	for len(vals)%maxPerPage == 0 {
		for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
			res, err := p.client.Validators(ctx, height, &page, &maxPerPage)
			if err != nil {
				// TODO: standardize errors on the RPC side
				if regexpMissingHeight.MatchString(err.Error()) {
					return nil, provider.ErrLightBlockNotFound
				}
				// if we have exceeded retry attempts then return no response error
				if attempt == maxRetryAttempts {
					return nil, provider.ErrNoResponse
				}
				// else we wait and try again with exponential backoff
				time.Sleep(backoffTimeout(uint16(attempt)))
				continue
			}
			if len(res.Validators) == 0 { // no more validators left
				valSet, err := types.ValidatorSetFromExistingValidators(vals)
				if err != nil {
					return nil, provider.ErrBadLightBlock{Reason: err}
				}
				return valSet, nil
			}
			vals = append(vals, res.Validators...)
			page++
			break
		}
	}
	valSet, err := types.ValidatorSetFromExistingValidators(vals)
	if err != nil {
		return nil, provider.ErrBadLightBlock{Reason: err}
	}
	return valSet, nil
}

func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) {
	for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
		commit, err := p.client.Commit(ctx, height)
		if err != nil {
			// TODO: standardize errors on the RPC side
			if regexpMissingHeight.MatchString(err.Error()) {
				return nil, provider.ErrLightBlockNotFound
			}
			// we wait and try again with exponential backoff
			time.Sleep(backoffTimeout(uint16(attempt)))
			continue
		}
		return &commit.SignedHeader, nil
	}
	return nil, provider.ErrNoResponse
}

func validateHeight(height int64) (*int64, error) {
	if height < 0 {
		return nil, fmt.Errorf("expected height >= 0, got height %d", height)
	}

	h := &height
	if height == 0 {
		h = nil
	}
	return h, nil
}

// exponential backoff (with jitter)
// 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
func backoffTimeout(attempt uint16) time.Duration {
	// nolint:gosec // G404: Use of weak random number generator
	return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
}
