package solana

import (
	"context"
	"errors"
	"net/http"
	"time"

	"github.com/avast/retry-go"
	"github.com/gagliardetto/solana-go"
	"github.com/gagliardetto/solana-go/rpc"
	"github.com/gagliardetto/solana-go/rpc/jsonrpc"
	"github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/metrics"
	"go.uber.org/ratelimit"
)

// https://github.com/solana-labs/solana/blob/master/rpc-client-api/src/custom_error.rs
const (
	//Slot was skipped, or missing due to ledger jump to recent snapshot
	ErrSlotSkippedCode = -32007

	//Slot was skipped, or missing in long-term storage
	ErrLongTermStorageSlotSkippedCode = -32009
)

const clientName = "solana"

var (
	ErrTooManyRequests = errors.New("too many requests")
	ErrSlotSkipped     = errors.New("slot was skipped")
)

type SolanaSDK struct {
	rpcClient  *rpc.Client
	commitment rpc.CommitmentType
	rl         ratelimit.Limiter
	retries    uint
	delay      time.Duration
	metrics    metrics.Metrics
}

type GetBlockResult struct {
	IsConfirmed  bool
	Transactions []rpc.TransactionWithMeta
	BlockTime    *time.Time
}

type GetLatestBlockResult struct {
	Block     uint64
	Timestamp time.Time
}

type Options func(*SolanaSDK)

func NewSolanaSDK(url string, rl ratelimit.Limiter, metrics metrics.Metrics, opts ...Options) *SolanaSDK {
	r := &SolanaSDK{
		rpcClient:  rpc.New(url),
		commitment: rpc.CommitmentConfirmed,
		rl:         rl,
		retries:    0,
		delay:      0,
		metrics:    metrics,
	}

	for _, opt := range opts {
		opt(r)
	}

	return r
}

func WithRetries(retries uint, delay time.Duration) Options {
	return func(s *SolanaSDK) {
		s.retries = retries
		s.delay = delay
	}
}

func (s *SolanaSDK) GetLatestBlock(ctx context.Context) (*GetLatestBlockResult, error) {
	s.rl.Take()
	var slot uint64
	err := s.withRetry(func() error {
		var er error
		slot, er = s.rpcClient.GetSlot(ctx, s.commitment)
		return s.convertError("get-latest-block", er)
	})
	return &GetLatestBlockResult{Block: slot, Timestamp: time.Now()}, err
}

func (s *SolanaSDK) GetBlock(ctx context.Context, block uint64) (*GetBlockResult, error) {
	s.rl.Take()
	rewards := false
	maxSupportedTransactionVersion := uint64(0)
	var result *GetBlockResult
	err := s.withRetry(func() error {
		out, er := s.rpcClient.GetBlockWithOpts(ctx, block, &rpc.GetBlockOpts{
			Encoding:                       solana.EncodingBase64, // solana-go doesn't support json encoding.
			TransactionDetails:             "full",
			Rewards:                        &rewards,
			Commitment:                     s.commitment,
			MaxSupportedTransactionVersion: &maxSupportedTransactionVersion,
		})
		if er != nil {
			return s.convertError("get-block", er)
		}
		if out == nil {
			// Per the API, nil just means the block is not confirmed.
			result = &GetBlockResult{IsConfirmed: false}
			return nil
		}
		var blockTime *time.Time
		if out.BlockTime != nil {
			t := out.BlockTime.Time()
			blockTime = &t
		}
		result = &GetBlockResult{IsConfirmed: true, Transactions: out.Transactions, BlockTime: blockTime}
		return nil
	})
	return result, err
}

func (s *SolanaSDK) GetSignaturesForAddress(ctx context.Context, address solana.PublicKey) ([]*rpc.TransactionSignature, error) {
	s.rl.Take()
	var result []*rpc.TransactionSignature
	err := s.withRetry(func() error {
		var er error
		result, er = s.rpcClient.GetSignaturesForAddress(ctx, address)
		return er
	})
	return result, err
}

func (s *SolanaSDK) GetTransaction(ctx context.Context, txSignature solana.Signature) (*rpc.GetTransactionResult, error) {
	s.rl.Take()
	maxSupportedTransactionVersion := uint64(0)
	var result *rpc.GetTransactionResult
	err := s.withRetry(func() error {
		var er error
		result, er = s.rpcClient.GetTransaction(ctx, txSignature, &rpc.GetTransactionOpts{
			Encoding:                       solana.EncodingBase64,
			Commitment:                     s.commitment,
			MaxSupportedTransactionVersion: &maxSupportedTransactionVersion,
		})
		return s.convertError("get-transaction", er)
	})
	return result, err
}

func (s *SolanaSDK) convertError(operation string, er error) error {
	if er == nil {
		s.metrics.IncRpcRequest(clientName, operation, http.StatusOK)
		return nil
	}

	errRet := er
	statusCode := http.StatusInternalServerError

	switch err := er.(type) {
	case *jsonrpc.RPCError:
		statusCode = err.Code
		switch err.Code {
		case http.StatusTooManyRequests:
			errRet = ErrTooManyRequests
		case ErrSlotSkippedCode, ErrLongTermStorageSlotSkippedCode:
			errRet = ErrSlotSkipped
		default:
			errRet = er
		}
	default:
		errRet = er
	}
	s.metrics.IncRpcRequest(clientName, operation, statusCode)
	return errRet
}

func (s *SolanaSDK) withRetry(fn func() error) error {
	return retry.Do(
		func() error {
			return fn()
		},
		retry.Attempts(s.retries),
		retry.Delay(s.delay),
		retry.RetryIf(
			func(err error) bool {
				return err == ErrTooManyRequests
			},
		),
		retry.LastErrorOnly(true),
	)
}
